1. 程式人生 > WINDOWS開發 >MVVM在WPF中的應用

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,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過於繁瑣的問題。