WPF 之 Binding 對數據的校驗與轉換(三)

一、前言

​ Binding 的作用就是架在 SourceTarget 之間的橋樑,數據可以在這座橋樑的幫助下來流通。就像現實中的橋樑會設置一些關卡進行安檢一樣,Binding 這座橋上也可以設置關卡對數據的有效性進行校驗。不僅如此,當兩端要求使用不同的數據類型時,我們還可以為數據設置轉換器。

​ Binding 用於數據有效性校驗的關卡是它的 ValidationRules 屬性,用於數據轉換的關卡是它的 **Converter ** 屬性。

二、Binding 對數據的校驗

​ 例如:我們把一個 Slider 的Value 屬性和 TextBox 的 Text 屬性雙向綁定在一起, Slider 的Value 的有效值是 0~100。當我們使用 Binding 綁定數值時,需要對該 Binding 的 ValidationRules 添加校驗規則。具體實現如下所示:

​ 第一步:我們聲明一個 RangeValidationRule 類編寫校驗規則,該類需要繼承 ValidationRule 類(該類為抽象類),ValidationRule類的作用是提供創建自定義規則的一個方式,旨在檢查用戶輸入的有效性。具體代碼如下:

  public class RangeValidationRule : ValidationRule
    {
        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
        {
            if (double.TryParse(value.ToString(), out var d))
            {
                if (d >= 0 && d <= 100)
                {
                    return new ValidationResult(true, null);
                }
            }
            return new ValidationResult(false, "Validation value");
        }
    }

​ 第二步:對 TextBox 的 Text 屬性進行 Binding 綁定,並對該 Binding 的 ValidationRules 添加一個校驗規則 RangeValidationRule 類,具體實現如下:

    <StackPanel Grid.Row="0">
           <TextBox x:Name="TextBox0" Margin="5" Height="50" VerticalContentAlignment="Center" ToolTip="{Binding ElementName=TextBox0,Path=e}">
               <TextBox.Text>
                   <Binding ElementName="Slider0" Path="Value" UpdateSourceTrigger="PropertyChanged">
                       <Binding.ValidationRules>
                           <local:RangeValidationRule ></local:RangeValidationRule>
                       </Binding.ValidationRules>
                   </Binding>
               </TextBox.Text>
           </TextBox>
            <Slider x:Name="Slider0" Margin="5" Minimum="0" Maximum="100"></Slider>
       </StackPanel>

然後,我們在文本框中,輸入200,發現 TextBox 會顯示紅色邊框,這表示數值是錯誤的,具體顯示如下圖:

image-20210205151612353

三、Binding 的數據轉換

​ 例如:我們實現如下一個程序,程序的用途是在列表裡向玩家顯示一些軍用飛機的狀態。數據類型如下:

  public enum Category
    {
        Bomber,
        Fighter,
    }

    public enum State
    {
        Available,
        Locked,
        Unknown,
    }

    public class Plane
    {
        public Category Category { get; set; }
        public string Name { get; set; }
        public State State { get; set; }
    }

​ 在UI中,Plane 的 Category 屬性被映射為圖片,State 被映射為 CheckBox 的 IsChecked 屬性。具體轉換規則如下:

public class CategoryToSourceConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var c = (Category)value;
            switch (c)
            {
                case Category.Bomber:
                    return "image/bomber.png";
                case Category.Fighter:
                    return "image/fighter.png";
            }
            return new ValidationResult(false, "Validation Value");
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }



    public class StateToBoolConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var s = (State) value;
            switch (s)
            {
                case State.Available:
                    return true;
                case State.Locked:
                    return false;
                default:
                    return null;
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var b = (bool?) value;
            switch (b)
            {
                case true:
                    return State.Available;
                case false:
                    return State.Locked;
                default:
                    return State.Unknown;
            }
        }
    }

​ 我們添加一個 ViewModel 綁定到 UI 上,ViewModel 代碼如下(屬性變更可以閱讀 ):

 public class Window3VM : NotifyProperty
    {
        private ObservableCollection<Plane> _planes;
        private string _output;

        public ObservableCollection<Plane> Planes
        {
            get => _planes;
            set => SetProperty(ref _planes, value);
        }

        public string Output
        {
            get => _output;
            set => SetProperty(ref _output, value);
        }
    }

​ 我們把 ViewModel 綁定到 UI 界面上,並把轉換規則添加到對應的 Binding 上,具體實現如下:

<Window x:Class="UI.Window3"
        xmlns="//schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="//schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="//schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="//schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:UI"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance local:Window3VM}"
        Title="Window3" Height="500" Width="600">
    <Window.Resources>
       <local:CategoryToSourceConverter x:Key="CTS"></local:CategoryToSourceConverter>
        <local:StateToBoolConverter x:Key="STB"></local:StateToBoolConverter>
    </Window.Resources>
   <Grid>
       <Grid.RowDefinitions>
           <RowDefinition Height="100"></RowDefinition>
           <RowDefinition Height="*"></RowDefinition>
       </Grid.RowDefinitions>
       <StackPanel Grid.Row="0">
           <TextBox x:Name="TextBox0" Margin="5" Height="50" VerticalContentAlignment="Center" ToolTip="{Binding ElementName=TextBox0,Path=e}">
               <TextBox.Text>
                   <Binding ElementName="Slider0" Path="Value" UpdateSourceTrigger="PropertyChanged">
                       <Binding.ValidationRules>
                           <local:RangeValidationRule ></local:RangeValidationRule>
                       </Binding.ValidationRules>
                   </Binding>
               </TextBox.Text>
           </TextBox>
            <Slider x:Name="Slider0" Margin="5" Minimum="0" Maximum="100"></Slider>
       </StackPanel>
        <Grid Grid.Row="1">
           <Grid.ColumnDefinitions>
               <ColumnDefinition Width="*"></ColumnDefinition>
               <ColumnDefinition Width="200"></ColumnDefinition>
           </Grid.ColumnDefinitions>
           <StackPanel Grid.Column="0" Margin="10">
               <ListBox Margin="5" Height="200" ItemsSource="{Binding Path=Planes}">
                   <ListBox.ItemTemplate>
                       <DataTemplate>
                           <StackPanel Orientation="Horizontal">
                               <Image Margin="0,2,0,2" Width="50" Source="{Binding Path=Category,Converter={StaticResource CTS}}"></Image>
                               <TextBlock Margin="0,2,0,2" Width="200" TextAlignment="Center" VerticalAlignment="Center" Text="{Binding Path=Name}"></TextBlock>
                               <CheckBox Margin="0,2,0,2" Width="20"  IsChecked="{Binding Path=State,Converter={StaticResource STB}}"></CheckBox>
                           </StackPanel>
                       </DataTemplate>
                   </ListBox.ItemTemplate>
               </ListBox>
               <Button  Margin="5" Height="50" Content="Load" Click="ButtonLoad_OnClick"></Button>
               <Button Margin="5" Height="50" Content="Save" Click="ButtonSave_OnClick"></Button>

           </StackPanel>
           <StackPanel Grid.Column="1">
               <TextBlock Margin="5,5,5,0" Text="Output Data:"></TextBlock>
               <TextBox x:Name="TextBox" Margin="5" Height="310" BorderBrush="Black" Text="{Binding Path=Output}"></TextBox>
           </StackPanel>
       </Grid>
    </Grid>
</Window>

​ 如下所示,當我們點擊Load 按鈕後,加載出來列表,點擊 Save 按鈕後,把整個列表輸出到右邊的文本框:

/// <summary>
    /// Window3.xaml 的交互邏輯
    /// </summary>
    public partial class Window3 : Window
    {
        private readonly Window3VM vm;
        public Window3()
        {
            InitializeComponent();

            this.DataContext= vm =new Window3VM();
        }

        private void ButtonLoad_OnClick(object sender, RoutedEventArgs e)
        {
            vm.Planes=new ObservableCollection<Plane>()
            {
                new Plane(){Name = "B-1", Category = Category.Bomber, State = State.Unknown,},
                new Plane(){Name = "F-35",Category = Category.Fighter,State = State.Unknown,},
                new Plane(){Name = "B-6", Category = Category.Bomber, State = State.Unknown,},
                new Plane(){Name = "F-22",Category = Category.Fighter,State = State.Unknown,},
                new Plane(){Name = "J-20",Category = Category.Fighter,State = State.Unknown,},
            };
        }

        
        private void ButtonSave_OnClick(object sender, RoutedEventArgs e)
        {
            StringBuilder sb = new StringBuilder();
            foreach (var plane in vm.Planes)
            {
                sb.Append($"{plane.Category},{plane.Name},{plane.State}\r\n");
            }

            vm.Output = sb.ToString();
        }
    }

image-20210205154832112

Tags: