WPF笔记

一、XAML代码界面

        创建项目后,系统自动生成窗口元素其内容为网格元素。
        格式:<元素名 属性=“值”>内容</元素名>
              或 <元素名 属性=“值”/>
              或 <元素名/>

窗口元素

        管理、配置、创建、显示用户与独立应用程序交互的窗口,可将控件元素或布局元素添加于<Window 属性=“值”>内容</ Window >的内容位置

自动生成属性 含义
x:Class 对应cs文件中partial关键字继承Window父类的同名类
xmlns 默认的命名空间
xmlns:x 映射为 x: 前缀的命名空间
xmlns:d 映射为 d: 前缀的命名空间
xmlns:mc 映射为 mc: 前缀的命名空间
xmlns:local 映射为 local: 前缀、含 x:Class 同名类和所有 XAML 文件的代码的命名空间
mc:Ignorable 忽略参数前缀中的设计特性 一般为d前缀名
Title 运行时窗口显示标题
Height 窗口高度
Width 窗口宽度
可选填属性 参数
WindowState 窗口状态:Maximized(全屏)、Minimized(最小化)、Normal(还原)
WindowStyle 边框类型:None (不显示标题栏和边框)、SingleBorderWindow(单个边框的窗口)、ThreeDBorderWindow(三维边框)、ToolWindow(固定工具窗口)
默认命名空间 含义
http://schemas.microsoft.com/winfx/2006/xaml/presentation 整个Windows Presentation Foundation (WPF)
http://schemas.microsoft.com/winfx/2006/xaml 单独的可扩展应用程序标记语言 (XAML)
http://schemas.microsoft.com/expression/blend/2008 设计器支持
http://schemas.openxmlformats.org/markup-compatibility/2006 表示并支持读取 XAML 的标记兼容性模式

二、布局元素

        项目自动生成的网格元素Grid是常用布局元素之一。布局元素的内容一般可放置多个控件或嵌套其它布局。

布局元素名 解释 特点
Grid 网格 行列摆放子元素,效果类似表格
StackPanel 栈式面板 水平或竖直排列子元素,移除一个元素自动前移补齐
Canvas 画布 坐标放置子元素
DockPanel 停靠面板 按上下左右中的区域放置子元素,最后一个子元素默认填充布局剩余空间
WrapPanel 自动折行面板 按布局大小自动子元素换行排列

1、Grid

定义Grid的行与列

        Grid类具有ColumnDefinitions和RowDefinitions两个属性,它们分别是ColumnDefinition和RowDefinition的集合,表示Grid定义了多少列、多少行。例如:

<Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
</Grid>

以上定义了3行3列。

设定行的高度和宽度

        列有宽度,行有高度

  • 绝对值:double数值加单位后缀;
  • 比例值:double数值后加一个星号(*);
  • 自动值:字符串AUTO。
英文名称 中文名称 简写 换算
Pixel 像素 px(默认,可省略) 图形基本单位
Inch 英寸 in 1 inch = 96 pixel
Centimeter 厘米 cm 1 cm = (96/2.54) pixel
Point pt 1 pt = (96/72) pixel

        以上为绝对值,即double数值加单位后缀。

        比例值则是把所有比例值的数值加起来作为分母、把每个比例值的数值作为分子,再把这个分数值乘以未被占用控件的像素数,得到的结果就是分配给这个比例值的最终像素数。比如,一个总高度为150px的Grid,有5行,其中两行采用绝对值25px,其他三行分别是2*、1*、2*,计算后,这三行分配的像素数应该是40px、20px和40px。
        如果使用自动值"AUTO",则有行列内的控件的高度和宽度决定,即控件会把行列撑到合适的宽度和高度。若行列中没有控件,则行高和列宽均为0。

示例:

<Grid Margin="10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="4" /><!-- 这是“提交”和“清除”两个按钮之间的空隙 -->
            <ColumnDefinition Width="80" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="25" />
            <RowDefinition Height="4" /><!-- 这是ComboBox与textbox之间的空隙 -->
            <RowDefinition Height="*" />
            <RowDefinition Height="4" /><!-- 这是textbox与按钮之间的空隙 -->
            <RowDefinition Height="25" />
        </Grid.RowDefinitions>
        <TextBlock Text="选择部门并留言:" Grid.Column="0" Grid.Row="0" VerticalAlignment="Center" />
        <ComboBox Grid.Column="1" Grid.Row="0" Grid.ColumnSpan="4" />
        <TextBox Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="5" />
        <Button Content="提交" Grid.Column="2" Grid.Row="4" />
        <Button Content="清除" Grid.Column="4" Grid.Row="4" />
    </Grid>

2、StackPanel

        可以把内部元素在纵向或横向上紧凑排列、形成栈式布局,通俗的讲就是像搭积木一样。基于这个特点,StackPanel适合的场景有:

  • 同类元素需要紧凑排列(如制作菜单或列表);
  • 移除其中的元素后能够自动补缺的布局或动画。

        StackPanel使用三个属性来控制内部元素的布局:

属性名称 数据类型 可取值 描述
Orientation Orientation枚举

Horizontal

Vertical

决定内部元素是横向累积还是纵向累积
HorizontalAlignment HorizontalAlignment枚举

Left

Center

Right

Stretch

决定内部元素水平方向上的对齐方式
VerticalAlignment VerticalAlignment枚举

Top

Center

Bottom

Stretch

决定内部元素竖直方向上的对齐方式

例如:

<Grid>
        <GroupBox Header="请选择没错别字的的成语" BorderBrush="Black" Margin="5">
            <StackPanel Margin="5">
                <CheckBox Content="A.迫不及待" />
                <CheckBox Content="B.妙不可言" />
                <CheckBox Content="C.唉声叹气" />
                <CheckBox Content="D.陈词滥调" />
                <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
                    <Button Content="确定" Width="60" Margin="5" />
                    <Button Content="取消" Width="60" Margin="5" />
                </StackPanel>
            </StackPanel>
        </GroupBox>
    </Grid>

3、Canvas

        当控件放在Canvas里就会被附加上Canvas.X和Canvas.Y属性。适用的场合包括:

  • 一经设计基本上不会再有改动的小型布局(如图标);
  • 艺术性比较强的布局;
  • 需要大量使用横纵坐标进行绝对定位的布局;
  • 依赖于横纵坐标的动画。

示例:

<Canvas>
        <TextBlock Text="用户名:" Canvas.Left="12" Canvas.Top="12" />
        <TextBox Height="23" Width="200" BorderBrush="Black" Canvas.Left="66" Canvas.Top="9" />
        <TextBlock Text="密码:" Canvas.Left="12" Canvas.Top="40.72" Height="16" Width="36" />
        <TextBox Height="23" Width="200" BorderBrush="Black" Canvas.Left="66" Canvas.Top="38" />
        <Button Content="确定" Width="80" Height="22" Canvas.Left="100" Canvas.Top="67" />
        <Button Content="清除" Width="80" Height="22" Canvas.Left="186" Canvas.Top="67" />
    </Canvas>

        以上是用Canvas代替Grid设计的登录窗口,除非确定这个窗口的布局以后不会改变且窗体尺寸固定,不然还是用Grid进行布局弹性会更好。

4、DockPanel

        DockPanel内的元素会被附加上DockPanel.Dock这个属性,这个属性的数据类型为Dock枚举,可取Left、Top、Right和Bottom四个值。根据Dock属性值,DockPanel内的元素会向指定方向累积、切分DockPanel内部的剩余可用空间,就像船舶靠岸一样。
        DockPanel还有一个重要属性——bool类型的LastChildFill,它的默认值是True。当它为True时,DockPanel内最后一个元素的DockPanel.Dock属性值会被忽略,这个元素会把DockPanel内部剩余空间撑满。

<Grid>
        <DockPanel>
            <TextBox DockPanel.Dock="Top" Height="50" BorderBrush="Blue" />
            <TextBox DockPanel.Dock="Left" Width="150" BorderBrush="BlueViolet" />
            <TextBox BorderBrush="Aqua"/>
        </DockPanel>
    </Grid>

        可以看到最顶上和最左边的设定了固定大小,右边这个作为最后一个元素则会把剩余空间撑满。 

5、WrapPanel

        WarpPanel内部采用的是流式布局,使用Orientation属性来控制流延伸的方向,使用HorizontalAlignment和VerticalAlignment两个属性控制内部控件的对齐。在流延伸的方向上,WrapPanel会排列尽可能多的控件,排不下的控件将会新起一行或一列继续排列。

<WrapPanel>
        <Button Width="200" Height="200" Content="OK" />
        <Button Width="200" Height="200" Content="OK" />
        <Button Width="200" Height="200" Content="OK" />
        <Button Width="200" Height="200" Content="OK" />
        <Button Width="200" Height="200" Content="OK" />
        <Button Width="200" Height="200" Content="OK" />
        <Button Width="200" Height="200" Content="OK" />
        <Button Width="200" Height="200" Content="OK" />
        <Button Width="200" Height="200" Content="OK" />
        <Button Width="200" Height="200" Content="OK" />
        <Button Width="200" Height="200" Content="OK" />
    </WrapPanel>

        更改窗体的尺寸。WrapPanel会调整内部控件的排列。 

三、Binding

1、两种写法:

XMAL代码

        路径(Path)就是用来设置binding要关联的那个属性,ElementName为绑定源对象的元素的名称。

        示例:

<TextBox x:Name="textbox1" Background="Aqua" Text="{Binding Path=Value,ElementName=Slider1}" />
<Slider x:Name="Slider1" Maximum="100" Minimum="0" />
<TextBox x:Name="textbox2" Background="Aquamarine" Text="{Binding Path=Text.Length,ElementName=textbox1,Mode=OneWay}" />//Mode为控制流方向

CS代码

Binding binding = new Binding(){Path = new PropertyPath("Value"), Source = Slider1};
BindingOperations.SetBinding(textbox3, TextBox.TextProperty, binding);

SetBinding (DependencyObject target, DependencyProperty dp, BindingBase binding);

        target DependencyObject:绑定的绑定目标

        dp DependencyProperty:绑定的目标属性

        binding BindingBase:描述绑定的BindingBase对象

XMAL代码:

<TextBox x:Name="textbox3" Text="{Binding Path=Value,ElementName=Slider1}" />

等价于C#代码:

this.textbox3.SetBinding(TextBox.TextProperty, new Binding("Value"){ElementName = "Slider1"});

2、控制Binding的方向及数据更新 

        有时数据只需要展示给用户、不允许用户修改,这时可以把Binding模式改为从源向目标的单向沟通,同时支持从目标向源的单向沟通和只在Binding关系确立时读取一次数据。
        控制Binding数据流向的属性是Mode,类型为BindingMode,默认值为Default,指的是根据目标的实际情况确定,比如若是可编辑的(如TextBox.Text属性),就采取双向模式TwoWay;若是只读的(如TextBlock.Text)则采用单项模式OneWay。

Mode属性 作用
TwoWay 只要目标属性或源属性发生更改,就很更新目标属性或源属性
OneWay 仅当源属性发生更改时更新目标属性
OneTime 仅当应用程序启动时或DataContext进行更改时目标属性
OneWayToSource 在目标属性更改时更新源属性
Default 默认值

        Binding还有一个属性——UpdateSourceTrigger,类型为UpdateSourceTrigger枚举,默认值为LostFocus,可取值PropertyChanged、LostFocus、Explicit和Default。将属性改为PropertyChanged,则上文中的Slider的手柄位置会即时地随TextBoxd里的值而改变。

参数值 作用
Explicit 仅在调用UpdateSource()方法更新源
LostFocus 默认值,一旦目标控件失去焦点,源就会被更新
PropertyChanged 一旦绑定的属性值改变,源会立即更新

3、Binding的路径(Path)

        Binding的Path属性来指定关注对象的哪个属性,简单例子:

<TextBlock x:Name="textBlock1" Background="Aquamarine" Text="{Binding Path=Value,ElementName=Slider1}" Margin="5" />//Value代表Slider目前的值
<Slider x:Name="Slider1" />
<TextBlock x:Name="textBlock2" Background="Aqua" Margin="5" />
Binding binding = new Binding { Path = new PropertyPath("Value"), Source = Slider1, };
textBlock2.SetBinding(TextBlock.TextProperty, binding);

        上面这个C#代码也可使用Binding的构造器简写为:

textBlock2.SetBinding(TextBlock.TextProperty, new Binding("Value") { Source = Slider1, });

        与上面第一点重复了

4、没有“Path”的Binding

        若Binding源本身就是数据且不用Path来指明,典型的string,int等基本类型就是这样,无法指出通过它的哪个属性来访问这个数据,这时只需将Path的值设置为“.”或省略就可以了,在XAML代码里这个“.”可以省略不写,但在C#代码中不能省略。

<StackPanel.Resources>
    <system:String x:Key="myString">
        菩提本无树,明镜亦非台。
        本来无一物,何处惹尘埃。
    </system:String>
</StackPanel.Resources>
<TextBlock x:Name="textBlock3" Background="Bisque" Margin="5" Text="{Binding Path=.,Source={StaticResource ResourceKey=myString}}" />

等效的C#代码如下:

string myString = "菩提本无树,明镜亦非台。本来无一物,何处惹尘埃。";
textBlock3.SetBinding(TextBlock.TextProperty, new Binding(".") {Source = myString});

5、使用DataContext作为Binding源

        WPF的UI布局是树形结构,每个结点都是控件,在UI元素树的每个结点都有DataContext。当一个Binding只知道自己的Path而不知道Source时,他会沿着UI元素树一路向树的根部找,每路过一个结点就看看这个结点的DataContext是否具有Path所指定的属性。若有,则把这个对象作为自己的Source;若无,则继续找如果到了树的根部仍没找到,那这个Binding就没有Source,因而也不会得到数据。示例:

        先定义一个类:

public class Student
    {
        public string Name { get; set; }
        public int    Id   { get; set; }
        public string Sex  { get; set; }
    }

        然后再XAML创建程序UI:

<Window x:Class="TestWPF.Test"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestWPF"//需要引入类所在命名空间
        mc:Ignorable="d"
        Title="Test" Height="450" Width="800">
    <StackPanel>
        <StackPanel.DataContext>
            <local:Student Id="10" Name="Mike" Sex="男" />
        </StackPanel.DataContext>
        <Grid>
            <StackPanel>
                <TextBlock x:Name="textBlock4" Background="Chocolate" Margin="5" Text="{Binding Path=Id}" />
                <TextBlock x:Name="textBlock5" Background="Teal" Margin="5" Text="{Binding Path=Name}" />
                <TextBlock x:Name="textBlock6" Background="DeepSkyBlue" Margin="5" Text="{Binding Path=Sex}" />
            </StackPanel>
        </Grid>
    </StackPanel>
</Window>

        使用xmlns:local="clr-namespace:TestWPF",就可以在XAML代码中使用在C#代码中定义的Student类,使用代码:

<StackPanel.DataContext>
    <local:Student Id="10" Name="Mike" Sex="男" />
</StackPanel.DataContext>

        就为外层StackPanel的DataContext进行了赋值,三个TextBlock的Text通过Binding获取值,但为Binding指定了Path、没有指定Source。这样,这三个TextBlock的Binding就会自动向元素树的上层去寻找可用的DataContext对象。最后它们在最外层的StackPanel上找到了可用的DataContext对象。结果如下:

         当Binding的Source本身就是数据,不需要属性时,Path和Source可省略不写,如:

<Window x:Class="TestWPF.Test"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:system="clr-namespace:System;assembly=mscorlib"//指定String类型所需
        mc:Ignorable="d"
        Title="Test" Height="450" Width="800">
    <StackPanel>
        <StackPanel.DataContext>
            <system:String>Hello Worldl!</system:String>
        </StackPanel.DataContext>
        <Grid>
            <StackPanel>
                <TextBlock Background="Aqua" Margin="5" Text="{Binding}" />
            </StackPanel>
        </Grid>
    </StackPanel>
</Window>

6、使用集合对象作为列表控件的ItemsSource

        WPF中的列表式控件们派生自ItemsControl类,自然也就继承了ItemsSource这个属性。ItemsSource属性可以接收一个IEnumerable接口派生类(子类)的实例作为自己的值(所有可被迭代遍历的集合都实现了这个接口,包括数组、List<T>等)。每个ItemControl都具有自己的条目容器(Item Container),例如,ListBox的条目容器是ListBoxItem、Combox的条目容器是ComboxItem。ItemSource里面保存的是一条一条的数据,想要把数据显示出来就要为数据穿上外衣,条目容器就起到了数据外衣的作用。这样将数据外衣和它所对应的条目容器关联起来呢?当然时依靠Binding!只要我们为一个ItemControl设置了ItemSource属性值,ItemControl会自动迭代其中的数据元素,为每个数据元素准备一个条目容器,并使用Binding元素在条目容器和数据元素之间建立起关联。示例:

<StackPanel x:Name="StackPanel" Background="LightBlue">
        <TextBlock Text="Student ID:" FontWeight="Bold" Margin="5" />
        <TextBox x:Name="TextBoxID" Margin="5" />
        <TextBlock Text="Student List:" FontWeight="Bold" Margin="5" />
        <ListBox x:Name="ListBoxStudents" Height="110" Margin="5">
</StackPanel>

        效果图如下: 

         我们要实现的效果是把一个List<Student>集合的实例作为ListBox的ItemsSource,让ListBox显示Student的Name并使用TextBox显示ListBox当前选中条目的Id。需在窗体的构造器中写代码:

public Test()
        {
            InitializeComponent();
            List<Student> stuList = new List<Student>
            {
                new Student { Id = 5, Name  = "Amy", Sex   = "女", },
                new Student { Id = 8, Name  = "John", Sex  = "男", },
                new Student { Id = 15, Name = "Mike", Sex  = "男", },
                new Student { Id = 29, Name = "Sarah", Sex = "女", },
            };

            //为ListBox设置Binding
            ListBoxStudents.ItemsSource       = stuList;
            ListBoxStudents.DisplayMemberPath = "Name";

            //为TextBox设置Binding
            Binding binding = new Binding("SelectedItem.Id") { Source = ListBoxStudents };
            TextBoxID.SetBinding(TextBox.TextProperty, binding);
        }

        运行效果图如下:

        ListBoxStudents.DisplayMemberPath = "Name",注意到 它包含“Path”这个单词,说明它是个路径。当DisplayMemberPath属性被赋值后,ListBox在获得ItemSource的时候就会创建等量的ListBoxItem并以DisplayMemberPath属性值为Path创建Binding。

        再看一个显式为数据设置DataTemplate的例子。先把C#代码中ListBoxStudents.DisplayMemberPath = "Name"删除,再在XAML中添加代码,ListBox的ItemTemplate属性的类型是DataTemplate,以下代码是为Student类型实例定做的:

<Window x:Class="TestWPF.Test"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="Test" Height="450" Width="800">
    <StackPanel x:Name="StackPanel" Background="LightBlue">
        <TextBlock Text="Student ID:" FontWeight="Bold" Margin="5" />
        <TextBox x:Name="TextBoxID" Margin="5" />
        <TextBlock Text="Student List:" FontWeight="Bold" Margin="5" />
        <ListBox x:Name="ListBoxStudents" Height="110" Margin="5">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Path=Id}" Width="30" />
                        <TextBlock Text="{Binding Path=Name}" Width="30" />
                        <TextBlock Text="{Binding Path=Sex}" Width="30" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </StackPanel>
</Window>

        结果如下:

         另外有一点:在使用集合类型作为列表控件的ItemsSource时一般会考虑使用ObservableCollection<T>代替List<T>,因为ObservableCollection<T>实现了INotifyCollectionChanged和INotifyPropertyChanged接口,能把集合的变化立刻通知显示它的的列表控件,改变会立刻显现出来。

7、使用XML数据作为Binding的源

        以下为基于DOM标准的XML类库。
        需要注意的是,当使用XML数据作为Binding的Source时我们将使用XPath属性而不是Path来指定数据的来源。

        以下示例:
        以下是一组学生信息的XML文本,存放于D:\Students.xml文件中。要把它显示在ListView中。

<?xml version="1.0" encoding="utf-8" ?>
<StudentList>
  <Student Id ="1">
    <Name>Tom</Name>
  </Student>
  <Student Id ="2">
    <Name>Jack</Name>
  </Student>
  <Student Id ="3">
    <Name>Joe</Name>
  </Student>
</StudentList>

        XAML部分如下:

<StackPanel>
<ListView x:Name="ListViewStudents" Height="130" Margin="5">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Id" Width="80" DisplayMemberBinding="{Binding XPath=@Id}" />
                    <GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding XPath=Name}" />
                </GridView>
            </ListView.View>
        </ListView>
        <Button Content="Load" Click="ButtonBase_OnClick" Height="25" Margin="5,0" />
</StackPanel>

        Butto的Click事件处理代码:

private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
            XmlDataProvider xdp = new XmlDataProvider();
            xdp.Source   = new Uri(@"D:\Students.xml");//Source可直接指定XML文档所在的位置
            xdp.XPath    = @"/StudentList/Student";

            ListViewStudents.DataContext = xdp;
            ListViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
        }

        XAML代码中最关键的两句是“DisplayMemberBinding="{Binding XPath=@Id}";”,和“DisplayMemberBinding={Binding XPath=Name}";”,它们分别为GridView的两列指明了关注的XML路径——明显,使用@符号加字符串表示的是XML元素的Attribute,不加@符号的字符串表示的是子级元素。

8、使用LINQ检索结果作为Binding的源

        LINQ查询的结果是一个IEnumerable<T>类型对象,而IEnumerable<T>又派生于IEnumerable,所以它可以作为列表控件的ItemsSource来使用。示例:

        UI界面:

<StackPanel>
<ListView x:Name="ListViewStudents1" Height="143" Margin="5">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}" />
                    <GridViewColumn Header="Name" Width="100" DisplayMemberBinding="{Binding Name}" />
                    <GridViewColumn Header="Sex" Width="60" DisplayMemberBinding="{Binding Sex}" />
                </GridView>
            </ListView.View>
        </ListView>
        <Button x:Name="btn1" Content="Load" Height="25" Margin="5,0" Click="btn1_Click"></Button>
</StackPanel>

        要从一个已经填充好的List<Student>对象中检索出所有女性,如下:

ListViewStudents1.ItemsSource = from student in stuList where student.Sex.Equals("女") select student;

           如果数据存储在XML文件中,如下:

四、Binding对数据的转换与校验

1、Binding的数据校验

        Binding的ValidationRules属性的类型是Collection<ValidationRule>,从它的名称可以看见,每个 Binding可以接收多个校验条件,每个条件都是ValidationRule类型对象。示例:检验TextBox的里输入的值是不是在0到100的范围内。

        首先准备放置一个TextBox和Slider,XAML代码:

<StackPanel>
    <TextBox x:Name="textBox1" Margin="5" />
    <Slider x:Name="slider1" Minimum="-10" Maximum="110" Margin="5" />
</StackPanel>

        为了进行校验,要准备一个ValidationRule派生类:

public class RangeValidationRule : ValidationRule
    {
        //需要实现Validate方法
        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
        {
            double d = 0;
            if (double.TryParse(value.ToString(), out d))
            {
                if (d >= 0 && d <= 100)
                {
                    return new ValidationResult(true, null);
                }
            }

            return new ValidationResult(false, "Validation Failed");
        }
    }

        ValidationRule类是抽象类,在该派生类中要实现Validate方法,该方法的返回值类型为ValidationResult类型,如果校验成功,ValidationResult的IsVaild的属性会被置为True;如果校验失败,ValidationResult的IsVaild的属性会被置为False,并为其ErrorContent属性设置一个合适的消息的内容(一般是个字符串)。

        然后再窗体的构造函数内建立Binding:

public MainWindow()
        {
            InitializeComponent();
 
            Binding binding = new Binding("Value") { Source = this.slider1 };
            binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            RangeValidationRule rule = new RangeValidationRule();
            binding.ValidationRules.Add(rule);
            this.textBox1.SetBinding(TextBox.TextProperty, binding);
        }

        这样当输入不在[0,100]范围内的数字时,TextBox的边框变红,表示校验错误:

         检验Source的数据:

        将Slider的调到(-∞,0)U(100,+∞),发现TextBox没有进行校验,这时因为Binding只对Binding目标输入时进行校验,对于Source(Slider)传来的数据不进行校验。如果想要设置同时校验Source数据,我们需要将校验规则的ValidatesOnTargetUpdated设置为True。

public MainWindow()
        {
            InitializeComponent();
 
            Binding binding = new Binding("Value") { Source = this.slider1 };
            binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            RangeValidationRule rule = new RangeValidationRule();
            rule.ValidatesOnTargetUpdated = true;在此
            binding.ValidationRules.Add(rule);
            this.textBox1.SetBinding(TextBox.TextProperty, binding);
        }

        校验失败的信息显示

        当校验失败时,Validate 返回的ValidationResult携带了一条错误信息,我们怎么在界面上显示这个错误信息呢?
        首先,需要把Binding对象的NotifyOnValidationError置为true。这样当数据校验失败时,Binding就会发出一个信息,以Binding对象的Target为起点,沿着UI元素树向上传播。信号每到达一个节点,如果节点设置了对这个信号的侦听器(事件处理器),那么这个侦听器就会被触发处理这个信号。这个传递过程称为路由(Route)。

public MainWindow()
        {
            InitializeComponent();
 
            Binding binding = new Binding("Value") { Source = this.slider1 };
            binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            RangeValidationRule rule = new RangeValidationRule();
            rule.ValidatesOnTargetUpdated = true;
            binding.ValidationRules.Add(rule);
 
            //校验失败发出信号
            binding.NotifyOnValidationError = true;
            this.textBox1.SetBinding(TextBox.TextProperty, binding);
 
            //关联事件处理器
            this.textBox1.AddHandler(Validation.ErrorEvent,new RoutedEventHandler(this.ValidationError));
        }

         //事件处理器
        void ValidationError(object sender, RoutedEventArgs e)
        {
            if (Validation.GetErrors(this.textBox1).Count > 0)
            {
                this.textBox1.ToolTip = Validation.GetErrors(this.textBox1)[0].ErrorContent.ToString();
            }
        }

2、Binding的数据转换

        实际的开发中,我们经常会遇到Binding的Source和Target是不同的类型,如下面的例子,我们需要将一个Button的IsEnable属性绑定到一个TextBox的Text属性,实现的效果是当TextBox的输入为空时,Button不可用。
        Bingding中有一个叫做Converter的属性,顾名思义,就是转换器的意思,就可以帮助我们实现这种效果。
        首先,我们需要自己写一个Converter,方法是创建一个类,继承IValueConverter接口:

public class StringToBoolConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
                return false;
 
            if (value is string&&
                string.IsNullOrEmpty((string)value)==false)
            {
                return true;
            }
            return false;
        }
 
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

        当数据从Binding的Source流向Target时,Convert方法将被调用;反之,ConvertBack将被调用。    

        在接口函数Convert中,我们进行从Binding的Source到Target类型的转换,在这里判断value是否为空,如果为空返回false,否则返回true;在ConvertBack中我们可以实现Binding的Target到Source类型的转换,同Convert的转换方法类似,在这里并没有实现,而是抛出了一个不用处理的NotImplementedException异常。

        然后在XAML中实现Binding,如下:

<Window x:Class="_6_34.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:_6_34"//命名空间
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:StringToBoolConverter x:Key="s2b"/>
    </Window.Resources>
        <Grid>
        <TextBox Height="23" HorizontalAlignment="Left" Margin="160,82,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" 
                 />
        <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="180,178,0,0" Name="button1" VerticalAlignment="Top" Width="75" 
                IsEnabled="{Binding Text,Converter={StaticResource s2b},ElementName=textBox1}"/>
    </Grid>
</Window>

        我们先在Window.Resources中创建了一个StringToBoolConverter对象,键为s2b,然后将Button的IsEnabled绑定到TextBox的Text属性上,Converter设置静态资源s2b,这样就实现了Binding的数据转换。

3、MultiBinding(多路Binding)

        有时候,UI需要的信息不止一个数据来源,这时候就需要使用多路绑定MultiBinding。下面我们实现通过两个TextBox来控制Button的可用性:两个TextBox的不为空且内容一致时,Button才可用;否则不可用。

        首先,搭建界面,XAML代码:

<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="192,191,0,0" Name="button1" VerticalAlignment="Top" Width="75" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="169,75,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="169,122,0,0" Name="textBox2" VerticalAlignment="Top" Width="120" />

        然后,创建一个Convert,此时需要继承自IMultiValueConverter,而不是之前的IValueConverter。

public class MultiBindingConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if(values.Cast<string>().Any(text=>string.IsNullOrEmpty(text))==false
                && values[0].ToString() == values[1].ToString())
            {
                return true;
            }
            return false;
        }
 
        public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

       在构造函数中实现多路绑定:

public MainWindow()
        {
            InitializeComponent();
 
            //准备binding
            Binding b1 = new Binding("Text") { Source = this.textBox1 };
            Binding b2 = new Binding("Text") { Source = this.textBox2 };
            //准备多路binding
            MultiBinding mb = new MultiBinding() { Mode=BindingMode.OneWay };
            mb.Bindings.Add(b1);
            mb.Bindings.Add(b2);
 
            MultiBindingConverter mbc=new MultiBindingConverter();
            mb.Converter = mbc;
 
            //设置关联
            this.button1.SetBinding(Button.IsEnabledProperty, mb);
        }

        结果如下:

posted @ 2023-10-26 09:27  时而有风  阅读(15)  评论(0编辑  收藏  举报