理解和使用WPF 驗證機制

首先建立一個demo用以學習和實驗WPF Data Validation機制。創建一個數據實體類:

public class Employee

{

    public string Name { get; set; }

    public int? Age { get; set; }

}

創建一個用戶控件或者窗口,用以輸入Name和Age,如下:

<Grid Width="400" Height="200">

        <Grid.RowDefinitions>

            <RowDefinition/>

            <RowDefinition/>

            <RowDefinition/>

        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>

            <ColumnDefinition Width="80"/>

            <ColumnDefinition/>

        </Grid.ColumnDefinitions>       

        <TextBlock Text="User Name:"VerticalAlignment="Center"/>

        <TextBox Grid.Column="1" x:Name="tb" Height="30">

            <TextBox.Text>

                <Binding Path="Name">

                    <Binding.ValidationRules>

                        <local:NotNullValidationRule/>

                    </Binding.ValidationRules>

                </Binding>

            </TextBox.Text>

        </TextBox>

 

        <TextBlock Text="Age" Grid.Row="1"VerticalAlignment="Center"/>

        <TextBox Grid.Row="1"Grid.Column="1" Height="30">

            <TextBox.Text>

                <Binding Path="Age">

                    <Binding.ValidationRules>

                        <local:NotNullValidationRule/>

                    </Binding.ValidationRules>

                </Binding>

            </TextBox.Text>

        </TextBox>

       

        <Button Grid.Row="2"Grid.Column="1" Content="Save" Width="60" Height="23"/>

    </Grid>

在後置代碼中連接數據上下文,如下:

void MainWindow_Loaded(object sender, RoutedEventArgs e)

{

    Employee p = new Employee();

    DataContext = p;

}

 

要運行此demo還需要創建一NotNullValidationRule 類,數據驗證的工作正是在此類中完成,此類的代碼如下:

public class NotNullValidationRule : ValidationRule

{

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)

    {

        if (value == null || string.IsNullOrWhiteSpace(value as string))

        {

            return new ValidationResult(false, "value cannot benull");

        }

 

        return ValidationResult.ValidResult;

    }

}

可以看到,此類必須從ValidationRule派生,然後重寫Validate方法,參數value就是設置數據綁定的控件屬性所表示的值。在我們的示例中,value就是TextBox.Text的值,也就是用戶輸入的文本。驗證邏輯非常簡單,不再贅述。驗證完成後需要返回一個ValidationResult 對象表示驗證結果,如果驗證的數據無效,就需要爲驗證結果指定一個字符串作爲錯誤信息反饋給用戶。

好了,現在demo可以運行了,在表示Name的文本框中輸入一些字符,然後刪除所有剛纔輸入的字符,最後按下tab鍵讓焦點離開改文本框。可以看見文本框出現了一個紅色邊框。顯然,紅色邊框不是很美觀,而且驗證錯誤信息也沒有通過Tooltip的方式呈現出來,記得以前是可以的,現在用的是.NetFramework 4.5,是沒有Tooltip提示的。下面我們就自定義一下驗證出錯時的UI顯示。

驗證錯誤的顯示樣式是由Validation.ErrorTemplate來控制的,這是一個關聯屬性,類型是ControlTemplate,下面是一個驗證錯誤控件模板的示例:

<ControlTemplate x:Key="ErrorTempalte">

            <StackPanel Orientation="Horizontal">

                <StackPanel.Triggers>

                    <EventTrigger RoutedEvent="FrameworkElement.Loaded" SourceName="bd">

                        <BeginStoryboard>

                            <Storyboard>

                                <DoubleAnimation Storyboard.TargetName="bd" Storyboard.TargetProperty="RenderTransform.ScaleX" From="0" To="1" Duration="0:0:0.2"/>

                            </Storyboard>

                        </BeginStoryboard>

                    </EventTrigger>

                </StackPanel.Triggers>

                <AdornedElementPlaceholder/>

                <Border CornerRadius="3"BorderBrush="DarkMagenta" Background="#AAFF0000" BorderThickness="1" Padding="5 2" x:Name="bd">

                    <Border.RenderTransform>

                        <ScaleTransform/>

                    </Border.RenderTransform>

                    <TextBlock Foreground="White" VerticalAlignment="Center" Text="{Binding Path=/ErrorContent}"/>

                </Border>

            </StackPanel>

        </ControlTemplate>

把以上模板應用到TextBox,

<TextBox Validation.ErrorTemplate="{StaticResource ErrorTempalte}">

再次運行demo,當焦點離開文本框後,會有一個紅色的錯誤提示顯示在文本框的右邊,而且還有一個X放心的放大動畫,使界面變得有一些動感了。對於這個ControlTemplate,需要記住的是,模板根元素的DataContext是一個ValidationError對象的列表,而在上面的模板中,我們將列表中第一個對象的ErrorContent顯示了出來(實際上,當一個綁定有多個ValidationRule的時候,WPF綁定引擎一旦發現有一個ValidationRule驗證失敗,那麼後續的ValidationRule將不會被執行),ErrorContent屬性就是我們在構造函數中指定的錯誤信息。另外一個需要注意的是,這個模板中的界面元素是顯示在AdornerLayer中的;要將顯示錯誤的界面元素顯示在被驗證控件的周圍,需要AdornedElementPlaceholder元素的支持,該元素是一個佔位符,跟被驗證控件有着同樣的位置和尺寸;而且還可以通過此類的AdornedElement屬性來訪問被驗證控件。

解決了錯誤信息顯示的問題,還有下一個問題等着我們解決;假設有一個添加用戶的界面,而且所有字段都是必須的,用戶在填寫了部分信息後,點擊了保存按鈕;如果我們直接保存,那麼大部分情況下會出錯,因爲還存在無效值。但是你可能會想,我們已經爲每個綁定設置了ValidationRule,爲什麼這些ValidationRule沒有起作用呢?這是因爲對於Binding來說,如果Target值沒有變化,那麼是不會引發驗證的;而且如果設置了綁定的UpdateSourceTrigger="LostFocus" 即使文本框的值變了,但是在文本框焦點離開之前,也是不會引發驗證。更糟糕的是,用戶根本就沒有對文本框做任何輸入,所以也就談不上焦點離開。所以這就要求我們在用戶點擊保存按鈕的時候,手動引發所有驗證操作,如果存在任何嚴重錯誤,那麼驗證錯誤就會像之前那樣滑動出來。所謂手動引發,指的是我們自己寫代碼去引發。可以想象,這不是一個輕鬆的工作,首先需要獲取所有的BindingExpressionBase對象,然後對每個綁定對象調用:

BindingExpressionBase exp = tb.GetBindingExpression(TextBox.TextProperty);

exp.ValidateWithoutUpdate();

顯然,這種方案是讓人無法忍受的。VaildationRule.ValidateOnTargetUpdated屬性或許會給我們帶來解決問題的曙光。修改NotNullValidationRule類如下:

public NotNullValidationRule()

{

    this.ValidatesOnTargetUpdated = true;

}

我們給NotNullValidationRule 添加了一個構造函數,其中把ValidatesOnTargetUpdated設置爲true;意思是,當Target被更新的時候執行驗證邏輯。當我們設置DataContext的時候,Target將會被更新。所以當我們做了這個修改後,重新運行demo,你會發現,窗口一出現,所有的驗證錯誤就被立刻顯示了出來。這顯然不是我們想要的,我們希望的邏輯是,當用戶第一次進入編輯界面,即使數據對象是無效的,也不要顯示驗證錯誤,只有當用戶點擊了保存按鈕或者焦點離開了某個綁定了數據對象的控件的時候才顯示驗證錯誤。由此我們得到的結論是:這個屬性也許在某個時候會有用處,但是現在對我們來說卻是無用的。

另一個解決此問題的方案是使用BindingGroup類,改類型是在.NetFramework 3.5 SP1引入的。BindingGroup能夠將一組綁定集合起來,整體更新。如下所示:

<Grid Width="400" Height="200">

        <Grid.BindingGroup>

            <BindingGroup x:Name="bg"/>

        </Grid.BindingGroup>

如果設置了Grid的BindingGroup屬性,那麼Grid裏面控件的所有綁定都會屬於同一個BindingGroup。實際上屬於不屬於同一個BindingGroup,主要是看Binding對象的BindingGroupName屬性是否和BindingGroup的Name相同,如果都沒有設置,自然就是相同的了。

添加了BindingGroup之後,我們需要在保存按鈕的事件處理方法中,驗證所有屬於BindingGroup的綁定,如下:

private void Button_Click(object sender, RoutedEventArgs e)

{

    bool isValid = bg.ValidateWithoutUpdate();

    if (isValid)

    {

        //Saveyour employee

    }

}

試着運行demo,你會發現,點擊了保存按鈕後,界面沒有任何響應,而且查看isValid的值,居然爲true。但是,假如嘗試着在Name和Age的文本框裏鍵入一些文本,再刪除這些鍵入的文本,然後點擊保存按鈕,驗證就會起作用。根本的原因是:BindingGroup總是認爲初始值是有效的值,不需要再驗證。這在編輯一個用戶的時候是有用的,但是因爲我們是在添加一個對象,我們的初始值都是無效的,所以BindingGroup這一特性使得我們無法完成驗證任務。BindingGroup還有一個UpdateSources方法,可以將所有屬於BindingGroup的BindingExpressionBase執行UpdateSource方法;但是這個方法有着同樣的缺陷,只對那些改變過的屬性進行更新。

而且BindingGroup還有另外一個特性也非常令人生厭,當你應用了BindingGroup後,它會改變屬於BindingGroup的Binding的UpdateSourceTrigger屬性的默認值爲Explicit,這就意味着,當焦點離開的時候,驗證將不會觸發;除非顯式調用每個BindingExpressionBase的UpdateSource方法,而這也是我們要使用的方法。但是顯示指定每個Binding的UpdateSourceTrigger會覆蓋這個行爲,BindingGroup會尊重你的設定。所以這基本上完成了我們的任務,改動的代碼如下:

<Grid Width="400" Height="200" x:Name="grid">

        <Grid.BindingGroup>

            <BindingGroup x:Name="bg"/>

 

<Binding Path="Name"UpdateSourceTrigger="LostFocus">

       <Binding Path="Age"UpdateSourceTrigger="LostFocus">…

                   

public MainWindow()

{

    InitializeComponent();

    Employee p = new Employee();

    DataContext = p;

}

保存按鈕事件處理方法:

private void Button_Click(object sender, RoutedEventArgs e)

{

    foreach (var item in bg.BindingExpressions)

    {

        item.UpdateSource();

    }                        

}

可以看到,問題的解決方案還是很簡單的。問題基本上解決了,但是對於仔細查看demo就會發現,當界面顯示以後,將焦點轉移到Name文本框上,然後再離開,這時候沒有發生驗證。這是可以理解的,因爲文本框裏的數據沒有發生變化,既然之前沒有顯示驗證錯誤,那麼現在也不應該顯示。如果一定要實現這個需求,就應該做出如下變化:

public MainWindow()

{

    InitializeComponent();

    Employee p = new Employee();

    DataContext = p;

    Loaded += MainWindow_Loaded;

    LostFocus += MainWindow_LostFocus;

}

 

void MainWindow_LostFocus(object sender, RoutedEventArgs e)

{

    foreach (var item in bg.BindingExpressions)

    {

        if (item.Target == e.Source)

        {

            item.UpdateSource();                   

            break;

        }               

    }

}

因爲LostFocus是一個路由事件,所以在主窗口中的LostFocus事件處理方法能夠處理所有包含控件的LostFocus事件。這方法裏面,我們獲取了當前失去焦點的控件的綁定對象,對其進行手動更新。注意到,我們顯式調用的UpdateSource,所以在Xaml中就不需要再設定UpdateSourceTrigger="LostFocus"了。

如果把上面的代碼封裝到一個關聯屬性裏,用起來會更方便,如下:

public static class FEExtension

    {

        public static bool GetValidateOnLostFocus(DependencyObject obj)

        {

            return (bool)obj.GetValue(ValidateOnLostFocusProperty);

        }

 

        public static void SetValidateOnLostFocus(DependencyObject obj, bool value)

        {

           obj.SetValue(ValidateOnLostFocusProperty, value);

        }

 

        public static readonly DependencyProperty ValidateOnLostFocusProperty =

            DependencyProperty.RegisterAttached("ValidateOnLostFocus", typeof(bool), typeof(FEExtension),

            new FrameworkPropertyMetadata(false, OnValidateOnLostFocusChanged));

 

        private static void OnValidateOnLostFocusChanged(object sender, DependencyPropertyChangedEventArgs e)

        {

            var fe = sender as FrameworkElement;

            if (e.NewValue.Equals(true))

            {

                fe.LostFocus += fe_LostFocus;

            }

            else

            {

                fe.LostFocus -= fe_LostFocus;

            }

        }

 

        static void fe_LostFocus(object sender, RoutedEventArgs e)

        {

            var fe = sender as FrameworkElement;

            foreach (var item in fe.BindingGroup.BindingExpressions)

            {

                if (item.Target == e.Source)

                {

                    item.UpdateSource();

                    break;

                }

            }

        }

    }

使用方法如下:

<Grid Width="400" Height="200" x:Name="grid" local:FEExtension.ValidateOnLostFocus="true">

深入理解ValidationRule

假設我們有一個畢業時間屬性,如下:

public class Employee

{

    public string Name { get; set; }

    public int? Age { get; set; }

    public DateTime GraduationDate { get; set; }

}

該屬性不能大於當前時間,所以需要創建一個驗證類如下:

class PassedDateTimeValidationRule : ValidationRule

{

    public PassedDateTimeValidationRule()

    {

        ValidationStep= ValidationStep.ConvertedProposedValue;

    }

 

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)

    {

        DateTime dt = (DateTime)value;

        if (dt < DateTime.Now)

        {

            return ValidationResult.ValidResult;

        }

 

        return new ValidationResult(false, "Cannot select afuture date");

    }

}

驗證邏輯非常簡單,需要注意的是構造函數中把ValidationStep設置爲ValidationStep.ConvertedProposedValue,這表示我們得到的參數value的值已經被Converter轉換過了,如果沒有Converter,WPF綁定引擎至少會幫你做一個類型轉換。所以在上例中,我們的value參數已經不是字符串了,而是一個DateTime對象。

ValidationStep是一個枚舉,每個值的解釋如下:

public enum ValidationStep

{

    // 使用原始值,對於TextBox來說,就是Text屬性表示的字符串

    RawProposedValue = 0,

    //

    // 使用轉換後的值,將原始值經過類型轉換或者經過BindingConverter轉換過的值,這個值

    // 還沒有被更新到我們的數據對象裏面。

    ConvertedProposedValue = 1,

    //

// 使用更新過的值,也就是說,我們的數據對象的屬性值已經被更新了,

// 然後用這個更新的值再做數據驗證

    UpdatedValue = 2,

    //

    // 使用提交後的值,驗證將會發生在調用了BindingGroup.CommitEdit之後。

    //大部分情況下我們都不會再數據提交了再做驗證,所以使用該值的情況應該非常少見。

    CommittedValue = 3,

}

對於ValidationStep.UpdatedValue,value參數會有所不同,value參數實際上是包含當前ValidationRule對象的BindingExpressionBase對象。說包含有些不恰當,因爲是Binding對象包含了ValidationRule。但是BindingExpressionBase和Binding之間是通過public屬性相互引用的。如果當前的ValidationRule是屬於BindingGroup的,那麼value參數就是BindingGroup對象,你可以對其進行轉換,這樣就可以方法BindingGroup公開的任何方法和屬性了。

深入理解BindingGroup

首先來看看BindingGroup提供了那些功能:

public class BindingGroup : DependencyObject

{

  //得到所有屬於當前BindingGroupBindingExpressionBase對象

  public Collection<BindingExpressionBase> BindingExpressions { get; }

  //獲取當前BindingGroup的所有數據上下文對象,在本文中只有一個,就是Employee對象

 public IList Items { get; }

 //該集合將在UpdateSources,ValidateWithoutUpdate,CommitEdit被調用的時候進行驗證調用

 public Collection<ValidationRule> ValidationRules { get; }

 //下面這三個方法對應IEditableObject的方法,調用下面的方法後,如果你的對象實現了IEditableObject接口,那麼你的方法將會被調用

  public void BeginEdit();

  public void CancelEdit();

  public bool CommitEdit();

 //下面兩個方法前面已經介紹過了

  public bool UpdateSources();

  public boolValidateWithoutUpdate();

}

BindingGroup提供了很多功能,但是確有一個缺陷。這就是我前面提到的,如果文本框裏的值沒有變化,即使是無效的,它也不會再次驗證,也就是說,他會假定初始數據都是有效的,在添加一個新的實體的時候,就無法完成驗證數據的功能。

前面說到,我們可以單獨調用每個binding對象的Update方法,但是,當你這麼做了之後,BindingGroup本身的ValidationRules就得不到執行了。

一個比較Hack的方法是,將每個綁定的NeedsVlidation屬性設置爲true,如下:

foreach (var item in bg.BindingExpressions)

            {

                typeof(BindingExpressionBase)

                    .GetProperty("NeedsValidation",System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)

                    .SetValue(item, true);

            }

 

            bg.ValidateWithoutUpdate();

如此,ValidateWithoutUpdate方法就顯的正常了。因爲用反射更改了BindingExpressionBase的內部屬性,所以說這個方法有點Hack。

 

參考文章:

http://msdn.microsoft.com/en-us/magazine/ff714593.aspx

http://blogs.msdn.com/b/vinsibal/archive/2008/08/11/wpf-3-5-sp1-feature-bindinggroups-with-item-level-validation.aspx

http://blogs.msdn.com/b/vinsibal/archive/2008/08/22/bindinggroups-and-ieditablecollectionview.aspx

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章