共用方式為


企業應用程式中的驗證

注意

本電子書於 2017 年春季出版,此後尚未更新。 這本書中有很多仍然有價值的,但一些材料已經過時。

任何接受使用者輸入的應用程式都應該確保輸入的內容有效。 例如,應用程式可以檢查輸入內容是否只包含特定範圍的字元、有特定的長度或符合特定的格式。 如不經驗證,使用者可能會提供導致應用程式失敗的資料。 驗證會強制執行商務規則,並防止攻擊者插入惡意數據。

在 Model-View-ViewModel (MVVM) 模式的內容中,通常需要有檢視模型或模型才能執行資料驗證,並向檢視發出任何驗證錯誤訊號,以便使用者更正錯誤。 eShopOnContainers 行動應用程式會執行檢視模型屬性的同步客戶端驗證,並醒目提示包含無效數據的控件,以及顯示錯誤訊息,告知用戶數據無效的原因。 圖 6-1 顯示 eShopOnContainers 行動裝置應用程式中執行驗證所涉及的類別。

Validation classes in the eShopOnContainers mobile app

圖 6-1:eShopOnContainers 行動應用程式中的驗證類別

需要驗證的檢視模型屬性類型為 ValidatableObject<T>,而且每個 ValidatableObject<T> 執行個體都已將驗證規則新增至其 Validations 屬性。 藉由呼叫 ValidatableObject<T> 執行個體的 Validate 方法,從檢視模型叫用驗證,這會擷取驗證規則,並針對 ValidatableObject<T>Value 屬性執行。 任何驗證錯誤都會放入 Errors 實例的 ValidatableObject<T> 屬性中,並 IsValid 更新 實例的 ValidatableObject<T> 屬性,以指出驗證是否成功或失敗。

屬性變更通知是由 ExtendedBindableObject 類別提供,因此 Entry 控制項可以繫結至檢視模型類別之 ValidatableObject<T> 執行個體的 IsValid 屬性,以接獲輸入資料是否有效的通知。

指定驗證規則

驗證規則是透過建立衍生自 IValidationRule<T> 介面的類別所指定,如下列程式碼範例所示:

public interface IValidationRule<T>  
{  
    string ValidationMessage { get; set; }  
    bool Check(T value);  
}

這個介面會指定驗證規則類別必須提供 booleanCheck 用來執行必要驗證的方法,以及 ValidationMessage 其值為驗證錯誤訊息的屬性,如果驗證失敗,則會顯示該屬性。

下列程式代碼範例顯示 IsNotNullOrEmptyRule<T> 驗證規則,用來在 eShopOnContainers 行動應用程式中使用模擬服務時,對 使用者 LoginView 輸入的使用者名稱和密碼執行驗證:

public class IsNotNullOrEmptyRule<T> : IValidationRule<T>  
{  
    public string ValidationMessage { get; set; }  

    public bool Check(T value)  
    {  
        if (value == null)  
        {  
            return false;  
        }  

        var str = value as string;  
        return !string.IsNullOrWhiteSpace(str);  
    }  
}

Check方法會傳回 ,boolean指出 value 自變數為 null、空白或只包含空格符。

雖然 eShopOnContainers 行動應用程式未使用,但下列程式代碼範例會顯示驗證電子郵件地址的驗證規則:

public class EmailRule<T> : IValidationRule<T>  
{  
    public string ValidationMessage { get; set; }  

    public bool Check(T value)  
    {  
        if (value == null)  
        {  
            return false;  
        }  

        var str = value as string;  
        Regex regex = new Regex(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$");  
        Match match = regex.Match(str);  

        return match.Success;  
    }  
}

Check方法會傳回 ,boolean指出 value 自變數是否為有效的電子郵件位址。 在 value 引數中搜尋第一次出現的規則運算式模式,即可達成此目的,此模式是在 Regex 建構函式中指定。 在輸入字串中是否找到正規表示式模式,可藉由檢查 Match 對象的 Success 屬性值來判斷。

注意

屬性驗證有時會牽涉到相依屬性。 例如,當屬性 A 的有效值集合相依於屬性 B 中曾設定的特定值時,這就是相依屬性。若要檢查屬性 A 的值是否為其中一個允許的值,則牽涉到擷取屬性 B 的值。此外,當屬性 B 的值變更時,屬性 A 也必須重新驗證。

將驗證規則新增至屬性

在 eShopOnContainers 行動應用程式中,檢視需要驗證的模型屬性宣告為 類型 ValidatableObject<T>,其中 T 是要驗證的數據類型。 下列程式碼範例會舉例兩個此類屬性:

public ValidatableObject<string> UserName  
{  
    get  
    {  
        return _userName;  
    }  
    set  
    {  
        _userName = value;  
        RaisePropertyChanged(() => UserName);  
    }  
}  

public ValidatableObject<string> Password  
{  
    get  
    {  
        return _password;  
    }  
    set  
    {  
        _password = value;  
        RaisePropertyChanged(() => Password);  
    }  
}

若要進行驗證,驗證規則必須新增至 Validations 每個 ValidatableObject<T> 實例的集合,如下列程式代碼範例所示:

private void AddValidations()  
{  
    _userName.Validations.Add(new IsNotNullOrEmptyRule<string>   
    {   
        ValidationMessage = "A username is required."   
    });  
    _password.Validations.Add(new IsNotNullOrEmptyRule<string>   
    {   
        ValidationMessage = "A password is required."   
    });  
}

這個方法會將 IsNotNullOrEmptyRule<T> 驗證規則新增至每個 ValidatableObject<T> 執行個體的 Validations 集合,指定驗證規則的 ValidationMessage 屬性值,這會指定驗證失敗時所顯示的驗證錯誤訊息。

觸發驗證

eShopOnContainers 行動應用程式中所使用的驗證方法可以手動觸發屬性的驗證,並在屬性變更時自動觸發驗證。

手動觸發驗證

您可以手動觸發檢視模型屬性的驗證。 例如,當用戶點選 上的 LoginView[登入] 按鈕時,就會在 eShopOnContainers 行動裝置應用程式中,使用模擬服務時發生此情況。 命令委派會呼叫 LoginViewModel 中的 MockSignInAsync 方法,這會執行 Validate 方法以叫用驗證,如下列程式碼範例所示:

private bool Validate()  
{  
    bool isValidUser = ValidateUserName();  
    bool isValidPassword = ValidatePassword();  
    return isValidUser && isValidPassword;  
}  

private bool ValidateUserName()  
{  
    return _userName.Validate();  
}  

private bool ValidatePassword()  
{  
    return _password.Validate();  
}

方法Validate會叫用每個ValidatableObject<T>實例上的 Validate 方法,對使用者LoginView輸入的使用者名稱和密碼執行驗證。 下列程式代碼範例顯示 類別的 ValidatableObject<T> Validate 方法:

public bool Validate()  
{  
    Errors.Clear();  

    IEnumerable<string> errors = _validations  
        .Where(v => !v.Check(Value))  
        .Select(v => v.ValidationMessage);  

    Errors = errors.ToList();  
    IsValid = !Errors.Any();  

    return this.IsValid;  
}

這個方法會 Errors 清除集合,然後擷取加入至物件 Validations 集合的任何驗證規則。 每個擷取驗證規則的 Check 方法都會被執行,而無法驗證資料之任何驗證規則的 ValidationMessage 屬性值都會新增至 ValidatableObject<T> 執行個體的 Errors 集合。 最後設定 IsValid 屬性,並將其值傳回給呼叫方法,指出驗證成功或失敗。

在屬性變更時觸發驗證

每當系結屬性變更時,也可以觸發驗證。 例如,當 LoginView 中的雙向繫結設定 UserNamePassword 屬性時,就會觸發驗證。 下列程式碼範例會示範觸發流程:

<Entry Text="{Binding UserName.Value, Mode=TwoWay}">  
    <Entry.Behaviors>  
        <behaviors:EventToCommandBehavior  
            EventName="TextChanged"  
            Command="{Binding ValidateUserNameCommand}" />  
    </Entry.Behaviors>  
    ...  
</Entry>

Entry 控制項繫結至 ValidatableObject<T> 執行個體的 UserName.Value 屬性,而控制項的 Behaviors 集合中則新增了 EventToCommandBehavior 執行個體。 此行為會ValidateUserNameCommand執行 ,以回應 上的 Entry[TextChanged] 事件引發,而當變更中的Entry文字時,就會引發此事件。 接著,ValidateUserNameCommand 委派會執行 ValidateUserName 方法,在 ValidatableObject<T> 執行個體上執行 Validate 方法。 因此,每當使用者在 Entry 控制項中輸入使用者名稱的字元時,就會對輸入資料執行驗證。

如需行為的詳細資訊,請參閱 實作行為。

顯示驗證錯誤

eShopOnContainers 行動應用程式會醒目提示使用者任何驗證錯誤,方法是反白顯示包含無效數據的控件,並顯示錯誤訊息,告知使用者數據為何在包含無效數據的控件下方無效。 更正無效的數據時,該行會變更為黑色,並移除錯誤訊息。 圖 6-2 顯示當驗證錯誤存在時,eShopOnContainers 行動應用程式中的 LoginView。

Displaying validation errors during login

圖 6-2: 在登入期間顯示驗證錯誤

反白顯示包含無效數據的控制件

附加 LineColorBehavior 行為可用來醒目提示 Entry 發生驗證錯誤的控件。 下列程式代碼範例示範附加行為如何 LineColorBehavior 附加至 Entry 控件:

<Entry Text="{Binding UserName.Value, Mode=TwoWay}">
    <Entry.Style>
        <OnPlatform x:TypeArguments="Style">
            <On Platform="iOS, Android" Value="{StaticResource EntryStyle}" />
            <On Platform="UWP" Value="{StaticResource UwpEntryStyle}" />
        </OnPlatform>
    </Entry.Style>
    ...
</Entry>

控件 Entry 會取用明確的樣式,如下列程式代碼範例所示:

<Style x:Key="EntryStyle"  
       TargetType="{x:Type Entry}">  
    ...  
    <Setter Property="behaviors:LineColorBehavior.ApplyLineColor"  
            Value="True" />  
    <Setter Property="behaviors:LineColorBehavior.LineColor"  
            Value="{StaticResource BlackColor}" />  
    ...  
</Style>

此樣式會ApplyLineColor設定 控件上Entry附加行為的 和 LineColor 附加屬性LineColorBehavior。 如需樣式的詳細資訊,請參閱樣式

設定或變更附加屬性的值 ApplyLineColor 時, LineColorBehavior 附加行為會 OnApplyLineColorChanged 執行 方法,如下列程式代碼範例所示:

public static class LineColorBehavior  
{  
    ...  
    private static void OnApplyLineColorChanged(  
                BindableObject bindable, object oldValue, object newValue)  
    {  
        var view = bindable as View;  
        if (view == null)  
        {  
            return;  
        }  

        bool hasLine = (bool)newValue;  
        if (hasLine)  
        {  
            view.Effects.Add(new EntryLineColorEffect());  
        }  
        else  
        {  
            var entryLineColorEffectToRemove =   
                    view.Effects.FirstOrDefault(e => e is EntryLineColorEffect);  
            if (entryLineColorEffectToRemove != null)  
            {  
                view.Effects.Remove(entryLineColorEffectToRemove);  
            }  
        }  
    }  
}

這個方法的參數會提供附加行為的控件實例,以及附加屬性的 ApplyLineColor 舊值和新值。 如果ApplyLineColor附加屬性為 true,類別EntryLineColorEffect就會加入至控件的Effects集合,否則會從控件的Effects集合中移除。 如需行為的詳細資訊,請參閱 實作行為。

類別 EntryLineColorEffectRoutingEffect 子類別,如下列程式代碼範例所示:

public class EntryLineColorEffect : RoutingEffect  
{  
    public EntryLineColorEffect() : base("eShopOnContainers.EntryLineColorEffect")  
    {  
    }  
}

類別 RoutingEffect 代表平台獨立效果,這個效果會包裝平臺特定的內部效果。 這可簡化效果移除程序,因為對於平台特定效果,並不存在對類型資訊的編譯時間資訊存取。 會 EntryLineColorEffect 呼叫基類建構函式,傳入由解析組名串連組成的參數,以及每個平臺特定效果類別上指定的唯一標識符。

下列程式代碼範例顯示 iOS 的 eShopOnContainers.EntryLineColorEffect 實作:

[assembly: ResolutionGroupName("eShopOnContainers")]  
[assembly: ExportEffect(typeof(EntryLineColorEffect), "EntryLineColorEffect")]  
namespace eShopOnContainers.iOS.Effects  
{  
    public class EntryLineColorEffect : PlatformEffect  
    {  
        UITextField control;  

        protected override void OnAttached()  
        {  
            try  
            {  
                control = Control as UITextField;  
                UpdateLineColor();  
            }  
            catch (Exception ex)  
            {  
                Console.WriteLine("Can't set property on attached control. Error: ", ex.Message);  
            }  
        }  

        protected override void OnDetached()  
        {  
            control = null;  
        }  

        protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)  
        {  
            base.OnElementPropertyChanged(args);  

            if (args.PropertyName == LineColorBehavior.LineColorProperty.PropertyName ||  
                args.PropertyName == "Height")  
            {  
                Initialize();  
                UpdateLineColor();  
            }  
        }  

        private void Initialize()  
        {  
            var entry = Element as Entry;  
            if (entry != null)  
            {  
                Control.Bounds = new CGRect(0, 0, entry.Width, entry.Height);  
            }  
        }  

        private void UpdateLineColor()  
        {  
            BorderLineLayer lineLayer = control.Layer.Sublayers.OfType<BorderLineLayer>()  
                                                             .FirstOrDefault();  

            if (lineLayer == null)  
            {  
                lineLayer = new BorderLineLayer();  
                lineLayer.MasksToBounds = true;  
                lineLayer.BorderWidth = 1.0f;  
                control.Layer.AddSublayer(lineLayer);  
                control.BorderStyle = UITextBorderStyle.None;  
            }  

            lineLayer.Frame = new CGRect(0f, Control.Frame.Height-1f, Control.Bounds.Width, 1f);  
            lineLayer.BorderColor = LineColorBehavior.GetLineColor(Element).ToCGColor();  
            control.TintColor = control.TextColor;  
        }  

        private class BorderLineLayer : CALayer  
        {  
        }  
    }  
}

OnAttached方法會擷取控件的Xamarin.FormsEntry原生控件,並藉由呼叫 UpdateLineColor 方法來更新線條色彩。 覆OnElementPropertyChanged寫會藉由更新附加LineColor屬性變更EntryHeight變更的 屬性來更新線條色彩,以回應控件上的Entry可系結屬性變更。 如需效果的詳細資訊,請參閱效果

當控件中 Entry 輸入有效的數據時,它會將黑色線條套用至控件底部,表示沒有驗證錯誤。 圖 6-3 顯示此範例。

Black line indicating no validation error

圖 6-3:指出沒有驗證錯誤的黑色線條

控件 Entry 也已 DataTrigger 加入至其 Triggers 集合。 下列程式代碼範例顯示 DataTrigger

<Entry Text="{Binding UserName.Value, Mode=TwoWay}">  
    ...  
    <Entry.Triggers>  
        <DataTrigger   
            TargetType="Entry"  
            Binding="{Binding UserName.IsValid}"  
            Value="False">  
            <Setter Property="behaviors:LineColorBehavior.LineColor"   
                    Value="{StaticResource ErrorColor}" />  
        </DataTrigger>  
    </Entry.Triggers>  
</Entry>

這會DataTrigger監視 UserName.IsValid 屬性,如果其值為 false,則會執行 Setter,這會將附加行為的附加屬性LineColorBehavior變更LineColor為紅色。 圖 6-4 顯示此範例。

Red line indicating validation error

圖 6-4:指出驗證錯誤的紅線

當輸入的數據無效時,控件中的 Entry 行會保持紅色,否則會變更為黑色,表示輸入的數據有效。

如需觸發程式的詳細資訊,請參閱 觸發程式

顯示錯誤訊息

UI 會在每個資料驗證失敗之控制項下方的 Label 控制項中顯示驗證錯誤訊息。 下列程式代碼範例顯示 Label 如果使用者尚未輸入有效的用戶名稱,則會顯示驗證錯誤訊息:

<Label Text="{Binding UserName.Errors, Converter={StaticResource FirstValidationErrorConverter}}"  
       Style="{StaticResource ValidationErrorLabelStyle}" />

每個 Label 都會系結至 Errors 正在驗證之檢視模型對象的屬性。 Errors 屬性是由 ValidatableObject<T> 類別提供,且類型為 List<string>。 因為 Errors 屬性可以包含多個驗證錯誤,所以使用 FirstValidationErrorConverter 執行個體擷取集合中的第一個錯誤以供顯示。

摘要

eShopOnContainers 行動應用程式會執行檢視模型屬性的同步客戶端驗證,並藉由醒目提示包含無效數據的控件,以及顯示錯誤訊息來通知用戶數據無效的原因。

需要驗證的檢視模型屬性類型為 ValidatableObject<T>,而且每個 ValidatableObject<T> 執行個體都已將驗證規則新增至其 Validations 屬性。 藉由呼叫 ValidatableObject<T> 執行個體的 Validate 方法,從檢視模型叫用驗證,這會擷取驗證規則,並針對 ValidatableObject<T>Value 屬性執行。 任何驗證錯誤都會放入 Errors 實例的 ValidatableObject<T>屬性中,並 IsValid 更新 實例的 ValidatableObject<T> 屬性,以指出驗證是否成功或失敗。