MVVM在WPF中的应用

Binding用于绑定控件属性的值。


Binding的模型

从Binding模型中可以看出,Binding对象作为目标和源之间的桥梁,除了著名的双向绑定特征外,WPF还在Binding中添加了一些机制方便我们更加方便的处理数据,比如校验器和转换器。


下面就来看看Binding对象到底实现了哪些属性?

1.TargetObject和Property属性默认不需要设置,在设置Binding的时候,WPF框架本身会根据你设置的属性及其控件自动引用。

2.Source属性设置:绑定一个数据源,一般是一个对象。常用的是DataContext和ItemSource。

①DataContext:每一个UI元素都有一个DataContext属性,这个属性属于依赖属性,也就是他是依赖在UI控件树的,当前控件没有指定DataContext时就会去父控件继续绑定。一般我们会将ViewModel赋给DataContext。

②ItemSource:列表控件绑定的对象。

另外还有XML数据、RelativeSource(相对自身可以指定层级的数据源)等形式绑定数据源。

3.Path属性:绑定数据源内的属性。

4.ElementName可以指定绑定UI元素为源,Converter转换器(数据源的属性类型转换成目标属性的类型),Validation校验器(设置一些规则校验数据),Mode绑定模式。

示例:

(一)ElementName、Mode

<StackPanel Orientation="Vertical" VerticalAlignment="Center">
    <TextBox x:Name="box1" />
    <TextBox x:Name="box2" Text="{Binding ElementName=box1,Path=Text,Mode=TwoWay}"/>
</StackPanel>

运行此示例,可以发现虽然Mode绑定为TwoWay模式,还是只能实现box2随box1内容同步,而在box1获取焦点时,才会同步box2的内容。从而看出,TwoWay并不是表示实时双向绑定。

可以选择box1和box2相互绑定的方式实现双向绑定。

<StackPanel Orientation="Vertical" VerticalAlignment="Center">
    <TextBox x:Name="box1" Text="{Binding ElementName=box2,Path=Text}"/>
    <TextBox x:Name="box2" Text="{Binding ElementName=box1,Path=Text}"/>
</StackPanel>

(二)Source、Converter

首先我们先新建一个Student的Model及Gender枚举类型表示学生的性别。

 public class Student
 {
     public int Number { get; set; }
     public string Name { get; set; }
     public Gender Gender { get; set; }
     public string Address { get; set; }
 }

 public enum Gender
 {
     Male,
     Female
 }

界面需要展示学生的编号、姓名、性别及住址,我们可以使用Binding绑定相应TextBlock的Text属性。

	<Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="Height" Value="30"/>
            <Setter Property="Width" Value="300"/>
            <Setter Property="Margin" Value="5"/>
            <Setter Property="HorizontalAlignment" Value="Center"/>
        </Style>
    </Window.Resources>
    <Grid>
        <StackPanel Orientation="Vertical" VerticalAlignment="Center">
            <TextBlock Text="{Binding Number}"/>
            <TextBlock Text="{Binding Name}"/>
            <TextBlock Text="{Binding Gender}"/>
            <TextBlock Text="{Binding Address}"/>
        </StackPanel>
    </Grid>

在后台代码中,我们只需要在构造当前Window时,给当前Window的DataContext属性添加一个Student类型的Model,这个Model可以来自http请求或其他的控件、本地数据库等等。

 public MainWindow()
 {
      InitializeComponent();
      DataContext = new Student
 	  {
           Number = 1,
           Name = "张三",
           Gender = Gender.Male,
           Address = "福州市"
      };
 }

运行程序,看到我们正确的在界面显示了绑定的属性的值:

可以看到,枚举类型并没有正确的显示成我们想要的男或女的文本形式,这时候我们就可以用到Converter。新建一个类,继承IValueConverter。

    public class GenderToStringConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var gender = (Gender)value;
            switch (gender)
            {
                case Gender.Male:
                    return "男";
                case Gender.Female:
                    return "女";
            }
            return null;
        }

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

在界面中需要在Resources中引用,并设置一个key,然后在相应的Binding中添加Converter引用。

<Window.Resources>
    <local:GenderToStringConverter x:Key="GenderToStringConverter"/>
    <Style TargetType="TextBlock">
        <Setter Property="Height" Value="30"/>
        <Setter Property="Width" Value="300"/>
        <Setter Property="Margin" Value="5"/>
        <Setter Property="HorizontalAlignment" Value="Center"/>
    </Style>
</Window.Resources>
<Grid>
    <StackPanel Orientation="Vertical" VerticalAlignment="Center">
        <TextBlock Text="{Binding Number}"/>
        <TextBlock Text="{Binding Name}"/>
        <TextBlock Text="{Binding Gender,Converter={StaticResource GenderToStringConverter}}"/>
        <TextBlock Text="{Binding Address}"/>
    </StackPanel>
</Grid>

这下按照我们的想法正确的展示了。


有这样一个需求,在同一个界面中,有多个地方都Binding了Student的Name属性,那么我希望后台更改Student对象的Name值的时候,前台界面所绑定的地方全部都要更新。
现在我们实现了Binding数据,但是如果数据有变化,我们界面上的相应属性并不会实时的发生变化,这时候我们就需要引入一个ViewModel层连接View和Model,使View和Model能够实时双向通信。ViewModel继承INotifyPropertyChanged,用于通知属性变化。

INotifyPropertyChanged用于通知属性变化

用于通知属性更改的接口,需要实现PropertyChanged事件,一般常在属性的set访问器中。做到当属性值发生改变之后,通知相应名称的属性的属性值变化。

①新建一个MainWindowViewModel
public class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private string _name;
    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            _name = value;
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}
②View界面代码修改成
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
      <TextBlock Text="{Binding Name}"/>
      <TextBox Text="{Binding Name}" TextChanged="TextBox_TextChanged"/>
      <TextBox Text="{Binding Name}" TextChanged="TextBox_TextChanged"/>
</StackPanel>
③View.cs后台界面代码为
public partial class MainWindow : Window
{
    private MainWindowViewModel MainWindowViewModel;
    public MainWindow()
    {
        InitializeComponent();
        MainWindowViewModel = new MainWindowViewModel();
        DataContext = MainWindowViewModel;
    }

    private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        if (sender is TextBox textBox)
        {
             MainWindowViewModel.Name = textBox.Text;
        }
    }
}
最终我们实现效果

可以看到我们成功地实现了双向绑定,这使得所有绑定ViewModel中Name的属性都会实时更新,并且在后台代码中我们并没有去改变View的值,试想,我们从服务端获取到一个新的Model数据,那么我们只需要把新Model的属性赋值给ViewModel中的对应属性,ViewModel就会更新界面元素

至此,我们在WPF中利用Binding和INotifyPropertyChanged成功实现了MVVM模式,View和Model完全解耦,相比于MVP模式中Presenter接管所有而言,MVVM将界面交互部分逻辑移植到ViewModel中,减轻了Presenter过于繁琐的问题。
posted @ 2020-06-19 22:48  aaayi  阅读(368)  评论(0编辑  收藏  举报