1. 程式人生 > >【Win10】使用 ValidationAttribute 實現資料驗證

【Win10】使用 ValidationAttribute 實現資料驗證

原文: 【Win10】使用 ValidationAttribute 實現資料驗證

WPF 中資料驗證的方式多種多樣,這裡就不說了。但是,在 Windows Phone 8.1 Runtime 中,要實現資料驗證,只能靠最基礎的手動編寫條件判斷程式碼來實現。如果用過 ASP.NET MVC 的那套資料驗證的話,再來 WP8.1,那簡直就是回到原始社會的感覺。

現在,得益於大一統,mobile 端的 App 也能用上 ValidationAttribute 了!(主要是指 System.ComponentModel.DataAnnotations 這個名稱空間下的 Attribute)。

那麼,接下來我就用 ValidationAttribute 來做一個簡單的資料驗證 Demo。

一、準備 Model

本次 Demo 我打算做一個使用者註冊的 Demo,那麼使用者註冊的話,就需要填寫 Email、Password 之類的資訊,並且需要驗證是否已經填寫或者在正確範圍內。那麼我們先編寫一個最基礎的 Model,我就叫 User 類。

public class User
{
    public string Email
    {
        get;
        set;
    }

    public string Password
    {
        get;
        set;
    }

    
public int Age { get; set; } public string Address { get; set; } }

這裡我準備了 4 個屬性,分別是 Email、密碼、年齡和地址。其中 Email、密碼、年齡都是必填的,密碼這種還必須有長度限制。結合 ValidationAttribute,我們修改 User 類為如下:

public class User
{
    [Required(ErrorMessage = "請填寫郵箱"
)] [EmailAddress(ErrorMessage = "郵箱格式錯誤")] public string Email { get; set; } [Required(ErrorMessage = "請填寫密碼")] [StringLength(20, MinimumLength = 6, ErrorMessage = "密碼最少 6 位,最長 20 位")] public string Password { get; set; } [Range(18, 150, ErrorMessage = "不到 18 歲不能註冊,並請填寫合適範圍的值")] public int Age { get; set; } [StringLength(50, ErrorMessage = "地址太長")] public string Address { get; set; } }

二、如何驗證?

這裡我們先暫停下 Demo 的編寫。我們已經為屬性標註好了 ValidationAttribute,那麼怎麼知道這個 User 類的例項是否通過了驗證,而在驗證不通過的時候,又是哪個屬性出問題呢?既然 .Net 框架給了這些 ValidationAttribute,那麼肯定也給瞭如何獲取驗證結果的。查閱 ValidationAttribute 所在的名稱空間後,我們找到一個叫 Validator 的類,這個就是使用者獲取驗證結果的。

編寫測試程式碼:

private void GetValidationResult()
{
    User user = new User()
    {
        Email = "[email protected]",
        Password = "123",
        Age = 18,
        Address = "XYZ"
    };

    ValidationContext context = new ValidationContext(user);
    List<ValidationResult> results = new List<ValidationResult>();
    bool isValid = Validator.TryValidateObject(user, context, results, true);

    Debugger.Break();
}

在這段程式碼中,我們構造了一個 User 類的例項,並設定了一些屬性。你可以看見,其中 Password 屬性是不符合驗證的,因為長度不足。

接下來三行程式碼就是進行驗證。最後是斷點。

執行之後,我們可以發現,isValid 為 false,並且 results 裡面被填充了一個物件。

QQ截圖20151112172559

如果修改 Password 屬性為符合驗證要求的話,再次執行程式碼的話,那麼 isValid 就會變成 true,results 的 Count 屬性也會保持為 0。

所以驗證的結果就是存放在 results 物件當中。

三、資料繫結與 Validation 結合

再次回到 Demo 的編寫中,因為我們需要使用資料繫結,所以需要 User 類實現 INotifyPropertyChanged 介面,並且,對於驗證這個需求,我們應該新增是否驗證成功和驗證結果這兩個屬性。

因為驗證需求不僅僅是用在 User 類上,這裡我抽象出一個基類,叫 VerifiableBase。同時我再編寫一個叫 BindableBase 的基類,這個作為資料繫結模型的基礎,相當於 MVVMlight 中的 ObservableObject。

BindableBase:

public abstract class BindableBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public virtual void RaisePropertyChanged([CallerMemberName]string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual void Set<T>(ref T storage, T newValue, [CallerMemberName]string propertyName = null)
    {
        if (Equals(storage, newValue))
        {
            return;
        }
        storage = newValue;
        RaisePropertyChanged(propertyName);
    }
}

VerifiableBase:

public abstract class VerifiableBase : BindableBase
{
    private VerifiableObjectErrors _errors;

    public VerifiableBase()
    {
        _errors = new VerifiableObjectErrors(this);
    }

    public VerifiableObjectErrors Errors
    {
        get
        {
            return _errors;
        }
    }

    public bool IsValid
    {
        get
        {
            return _errors.Count <= 0;
        }
    }

    public override void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
        base.RaisePropertyChanged(propertyName);
        _errors = new VerifiableObjectErrors(this);
        base.RaisePropertyChanged(nameof(Errors));
        base.RaisePropertyChanged(nameof(IsValid));
    }
}

這裡我添加了 IsValid 屬性表示該物件是否驗證成功,新增 Errors 屬性存放具體的錯誤內容。

在屬性發生變更的情況下,我們必須重新對該物件進行驗證,因此 override 父類 BindableBase 中的 RaisePropertyChanged 方法,重新構建一個錯誤資訊物件,並通知 UI 這兩個屬性發生了變化。

VerifiableObjectErrors:

public class VerifiableObjectErrors : IReadOnlyList<string>
{
    private List<string> _messages = new List<string>();

    private List<ValidationResult> _results = new List<ValidationResult>();

    internal VerifiableObjectErrors(VerifiableBase verifiableBase)
    {
        ValidationContext context = new ValidationContext(verifiableBase);
        Validator.TryValidateObject(verifiableBase, context, _results, true);
        foreach (var result in _results)
        {
            _messages.Add(result.ErrorMessage);
        }
    }

    public int Count
    {
        get
        {
            return _messages.Count;
        }
    }

    public string this[int index]
    {
        get
        {
            return _messages[index];
        }
    }

    public string this[string propertyName]
    {
        get
        {
            foreach (var result in _results)
            {
                if (result.MemberNames.Contains(propertyName))
                {
                    return result.ErrorMessage;
                }
            }
            return null;
        }
    }

    public IEnumerator<string> GetEnumerator()
    {
        return _messages.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

這個物件是一個只讀集合(實現 IReadOnlyList<T> 介面)。在建構函式中,驗證 VerifiableBase 物件,並將驗證結果儲存起來。添加了一個引數型別為 string 型別的索引器,可以通過傳遞屬性名稱獲取該屬性的第一條錯誤訊息,如果該屬性驗證通過,沒有錯誤的話,則返回 null。

 

在這些基礎的都編寫完之後,修改最開始的 User 物件:

public class User : VerifiableBase
{
    private string _email;

    private string _password;

    private int _age;

    private string _address;

    [Required(ErrorMessage = "請填寫郵箱")]
    [EmailAddress(ErrorMessage = "郵箱格式錯誤")]
    public string Email
    {
        get
        {
            return _email;
        }
        set
        {
            Set(ref _email, value);
        }
    }

    [Required(ErrorMessage = "請填寫密碼")]
    [StringLength(20, MinimumLength = 6, ErrorMessage = "密碼最少 6 位,最長 20 位")]
    public string Password
    {
        get
        {
            return _password;
        }
        set
        {
            Set(ref _password, value);
        }
    }

    [Range(18, 150, ErrorMessage = "不到 18 歲不能註冊,並請填寫合適訪問的值")]
    public int Age
    {
        get
        {
            return _age;
        }
        set
        {
            Set(ref _age, value);
        }
    }

    [StringLength(50, ErrorMessage = "地址太長")]
    public string Address
    {
        get
        {
            return _address;
        }
        set
        {
            Set(ref _address, value);
        }
    }
}

四、在 UI 中顯示驗證

測試頁面我就叫 MainView,它的 ViewModel 則為 MainViewModel。

編寫 MainViewModel:

public class MainViewModel
{
    private RelayCommand _registerCommand;

    private User _user;

    public MainViewModel()
    {
        _user = new User();
    }

    public RelayCommand RegisterCommand
    {
        get
        {
            _registerCommand = _registerCommand ?? new RelayCommand(async () =>
            {
                StringBuilder sb = new StringBuilder();
                sb.AppendLine($"郵箱:{User.Email}");
                sb.AppendLine($"密碼:{User.Password}");
                sb.AppendLine($"年齡:{User.Age}");
                sb.AppendLine($"地址:{User.Address}");
                await new MessageDialog(sb.ToString()).ShowAsync();
            });
            return _registerCommand;
        }
    }

    public User User
    {
        get
        {
            return _user;
        }
    }
}

接下來編寫 MainView 的程式碼:

<Page x:Class="UWPValidationDemo.Views.MainView"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:UWPValidationDemo.Views"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:vm="using:UWPValidationDemo.ViewModels"
      mc:Ignorable="d">
    <Page.DataContext>
        <vm:MainViewModel></vm:MainViewModel>
    </Page.DataContext>
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel HorizontalAlignment="Center"
                    Width="450">
            <StackPanel Margin="10">
                <TextBox Header="郵箱"
                         Text="{Binding Path=User.Email,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></TextBox>
                <TextBlock Text="{Binding Path=User.Errors[Email]}"
                           Foreground="Red"></TextBlock>
            </StackPanel>
            <StackPanel Margin="10">
                <PasswordBox Header="密碼"
                             Password="{Binding Path=User.Password,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></PasswordBox>
                <TextBlock Text="{Binding Path=User.Errors[Password]}"
                           Foreground="Red"></TextBlock>
            </StackPanel>
            <StackPanel Margin="10">
                <TextBox Header="年齡"
                         Text="{Binding Path=User.Age,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></TextBox>
                <TextBlock Text="{Binding Path=User.Errors[Age]}"
                           Foreground="Red"></TextBlock>
            </StackPanel>
            <StackPanel Margin="10">
                <TextBox Header="地址"
                         Text="{Binding Path=User.Address,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></TextBox>
                <TextBlock Text="{Binding Path=User.Errors[Address]}"
                           Foreground="Red"></TextBlock>
            </StackPanel>
            <Button Content="註冊"
                    HorizontalAlignment="Center"
                    IsEnabled="{Binding Path=User.IsValid}"
                    Command="{Binding Path=RegisterCommand}"></Button>
            <StackPanel Margin="20">
                <TextBlock Text="所有錯誤:"></TextBlock>
                <ItemsControl ItemsSource="{Binding Path=User.Errors}"></ItemsControl>
            </StackPanel>
        </StackPanel>
    </Grid>
</Page>

TextBox、PasswordBox 用於填寫,需要注意的是,Mode 需要為 TwoWay,UpdateSourceTrigger 為 PropertyChanged。TwoWay 是因為需要將 TextBox 的值寫回 User 類的例項中,PropertyChanged 是因為我們需要實時更新驗證,而不是控制元件失去焦點才驗證。註冊按鈕則繫結 IsValid 到按鈕的 IsEnabled 屬性上。最後用一個 ItemsControl 來顯示 User 類例項的所有錯誤,ItemsControl 有一個 ItemsSource 屬性,繫結一個集合後,ItemsControl 將會顯示每一個集合中元素,如果有用過 ListView 的話應該會很熟悉。

五、執行

不填寫任何時:

QQ截圖20151112194023

填寫錯誤時:

QQ截圖20151112194124

正確填寫時:

QQ截圖20151112194217

QQ截圖20151112194321

六、結語

可見,通過將資料繫結和 Validation 結合起來後,我們再也不用寫一堆又長又臭的條件判斷程式碼了。(^o^)

另外,在上面的程式碼中,我們是在 Model 和 BindableBase 的繼承關係中插入一個 VerifiableBase 類。同理,對於 ViewModel,我們也能夠輕易寫出一個 VerifiableViewModelBase 出來,用於 ViewModel 屬性上的驗證。這裡就大家自己編寫了,最後放上這個 Demo 的程式碼:UWPValidationDemo.zip