之前大家寫代碼都喜歡用事件驅動,比如說滑鼠輸入的click事件、初始化的內容全部放在表單加載完畢的load事件,等等,里面包含了大量的由事件觸發后的業務處理代碼,導致了UI和業務邏輯高度耦合在一個地方,代碼難于維護、也難以優化,
我們這個章要講的內容是忘記我們的事件驅動、嘗試理解資料驅動,客戶端開發分層的話理論上就是資料層、業務邏輯層、UI層,相對于三層的話一般我們的代碼可以分為:
A:資料的持久化存盤;
B:資料的讀取和寫入;
C:業務邏輯處理;
D:界面業務邏輯處理后資料的展示,
E:界面與業務邏輯的互動,
在這樣的開發程序中A、B一般都是設計最滿意的地方,持久化程序做的既通用、又能清晰,持久化資料和物體類之間的定義、轉換,都是變動性最小、最穩定的,而C與客戶端的關系最緊密、變動也最大,大多數代碼都是集中在這里,D、E兩部分是負責顯示UI、和處理UI的互動邏輯,也有不少的代碼量,
顯然C部分是一個程式中,代碼量最多,隨著版本迭代最容易混亂的地方,所以我們應該重點把精力放在C部分,但是D、E兩個部分切因為和業務層緊密相連,C部分的頻繁改動很可能導致我們把本來屬于C部分的代碼寫入D、E部分里,比如表單或控制元件的Click、建構式、load里面,因為這2部分以訊息或者事件來與邏輯層溝通,所以一旦出現同一個資料需要在多處展示、修改時,用于同步邏輯得代碼就會變得復雜,代碼也會到處亂寫,因為在解決業務問題時,我們的重點在C部分,但是在解決UI互動問題的時候,D、E的UI展示又編程了我們的重點,思維來回的切換,導致我們寫出很多難以維護的代碼,
WPF中引入了Data Binding的概念,使用Data Binding配合屬性通知和資料模板,我們就可以把關注的D、E的展示層和C的業務邏輯層更好的分割開來,使我們把重點放在業務邏輯層,UI上的元素通過Data Binding可以和資料關聯上、一處資料可以和多處UI元素系結,也可以雙向系結,如果能很好的使用這個思路,我們就可以很好的實作了邏輯層和UI層的解耦,而且所有與業務邏輯相關的代碼都會處于業務邏輯層、用戶界面不包含任何代碼,
開頭講了這么說,就是想讓大家忘記之前的事件驅動寫代碼的方式,然后嘗試開始學習資料驅動寫代碼的方式,Data Binding就是第一步,
什么是Binding
我們先來看一個最簡單的例子,我們使用Binding來把一個元素的值系結到另外一個元素的值上,使用ElementName來指向對應的元素Name,Path來指向我們想系結的元素對應的屬性,該例子不包含任何后臺代碼:
<Window x:Class="BindingExample.MainWindow" 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:BindingExample" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <StackPanel> <TextBlock Text="{Binding ElementName=slider,Path=Value}"/> <Slider x:Name="slider" Maximum="100" Minimum="1"/> </StackPanel> </Grid> </Window>
我們把代碼跑起來效果圖如下,我們的TextBlock顯示的Text和Slider的滑動值系結在了一起:

在WPF中Binding可以通過呼叫類的INotifyPropertyChanged的實作自動通知功能使多個系結了屬性的UI元素自動更新UI,在WPF中依賴項屬性是個很重要的知識點,但是我覺得應該先講解bangding,在建立了資料驅動的思維,先去使用資料驅動,再去搞明白資料驅動的原理,而這個例子中我們使用Binding系結了Slider的屬性Value,再Slider 上按F12.進入到類的說明界面,我們看到了又一個Value屬性,還有一個屬性名為ValueProperty,型別為DependencyProperty的物件,他就是我們所說的依賴項屬性,這一章我們不講他,只講如何使用,當作普通屬性就好,
所以這個例子就是我們把一個Textlock的Text顯示內容通過Binding系結到了Slider的Value屬性上,而通過在屬性的Set方法中呼叫INotifyPropertyChanged的實作,所以TextBlock的Text能隨著Slider的Value變化跟著一起顯示對應的值就行,這里能理解到這樣就可以了,繼續往下,

既然2個元素可以系結一個屬性,隨著DataContext下對應屬性的值的變化而變化,就達到了我們要的目的,解耦業務層和UI層,我們通過業務層修改對應的屬性,達到更新UI得目的,UI通過更新對應的屬性,達到修改業務層得目的,這樣我們就可以把重點放在業務層,
通過這個原理,我們嘗試創建一個業務層和UI層互動的屬性,并系結它,通過屬性更新UI顯示結果,通過UI得互動修改屬性得值來達到更新業務層,這樣我們只需要關注業務層當前值的變化,
我們通過cs檔案設定一個簡單的屬性通知,我們把UI的顯示值和設定的Person下的Intelligence屬性系結在一起,如果UI變化了,Intelligence也變化,如果Intelligence變化了,UI也跟著變化,就實作了我們剛才計劃的一個業務邏輯層的值變化,直接影響系結的UI部分,UI的系結的值變化,可以直接再邏輯層處理,因為只是演示功能,所以我們沒有VM層,這里只演示值得變化,后面MVVM會講解DataContext,和MVVM分層,這里主要理解Binding得值可以雙向通知就可以了,
修改XAML代碼和CS為以下內容:
<Window x:Class="BindingExample.MainWindow" 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:BindingExample " mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <StackPanel> <TextBlock Text="{Binding Intelligence}"/> <Slider x:Name="slider" Maximum="100" Minimum="1" Value=https://www.cnblogs.com/duwenlong/p/"{Binding Intelligence}"/> </StackPanel> </Grid> </Window>
using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace BindingExample { /// <summary> /// MainWindow.xaml 的互動邏輯 /// </summary> public partial class MainWindow : Window { Person duwenlong; public MainWindow() { InitializeComponent(); duwenlong = new Person(); this.DataContext = duwenlong; } } public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private double _intelligence; public double Intelligence { get { return _intelligence; } set { _intelligence = value; Debug.WriteLine($"Intelligence as {Intelligence}"); if (this.PropertyChanged != null) { this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Intelligence")); } } } } }
我們再每次Set值得時候,向控制臺列印了一個訊息,Debug.WriteLine($"Intelligence as {Intelligence}");用來觀察是否實作了系結,我們觀察到,值再變化的時候,業務層和UI層是一起再變化的,到此刻我們得目的就達到了,因為基于這個功能,我們結合其他知識我們可以完成很多很多得功能,但是目前要理解和養成資料驅動得思維習慣,

到現在為止,我們也沒有在后臺寫業務代碼,因為我們的模板是解耦業務邏輯層和UI層,我們要學習的是通過Binding來實作業務邏輯的值變更、直接更新到UI層,
我們講解一下以上代碼:
在XAML檔案中我們創建了一個TextBlock 和一個Slider,2個控制元件,我們把TextBlock的Text屬性(用于顯示文本的屬性)設定為{Binding Intelligence},把Slider的Value屬性(滑塊的當前值)設定為{Binding Intelligence},
如果想使用系結,
1、XAML中就必須使用{Binding }這樣的寫法,后面跟的是屬性,而這個屬性是來自于當前類的DataContext中,this.DataContext物件是我們自己在cs代碼中賦值的,XAML元素通過Binding系結DataContext下的某個元素的值,來實作更改對應的屬性,
而后臺代碼中必須設定需要系結的物件到this.DataContext,這個物件(我們當前的Person)必須繼承自INotifyPropertyChanged,并且使用PropertyChanged來觸發通知,如果這個屬性需要通知UI層,在屬性的Set里就需要發送通知訊息,寫法就類似于
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));
如果執行系結失敗可以在對照一下代碼,看看哪里有問題,這是簡單的系結,,
接下來我們嘗試雙向系結和通過代碼設定系結,修改代碼如下:
<Window x:Class="BindingExample.MainWindow" 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:BindingExample" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <StackPanel> <TextBlock Text="{Binding Intelligence}"/> <TextBox Text="{Binding Intelligence,Mode=TwoWay}"/> <Slider Minimum="1" Maximum="100" Value=https://www.cnblogs.com/duwenlong/p/"{Binding Intelligence}"/> <StackPanel Orientation="Horizontal"> <TextBlock Text="名稱:"/> <TextBlock Text="{Binding Name}" MinWidth="120"/> <TextBlock Text="請輸入需要修改的名稱:"/> <TextBox MinWidth="120" x:Name="tb_inputName"/> </StackPanel> <Button Content="通過代碼修改系結值得屬性,修改Name為杜文龍" Click="AlertText_Click"/> </StackPanel> </Grid> </Window>
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace BindingExample { /// <summary> /// MainWindow.xaml 的互動邏輯 /// </summary> public partial class MainWindow : Window { Person duwenlong; public MainWindow() { InitializeComponent(); duwenlong = new Person(); Binding binding = new Binding(); binding.Source = duwenlong;
binding.Mode = BindingMode.TwoWay; binding.Path = new PropertyPath("Name"); BindingOperations.SetBinding(tb_inputName, TextBox.TextProperty, binding); this.DataContext = duwenlong; } private void AlertText_Click(object sender, RoutedEventArgs e) { duwenlong.Name = "杜文龍"; } } public class Person : INotifyPropertyChanged { private double _intelligence; public double Intelligence { get { return _intelligence; } set { _intelligence = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Intelligence")); } } private string _name; public string Name { get { return _name; } set { _name = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name")); } } public event PropertyChangedEventHandler PropertyChanged; } }
在XAML中我們添加了一個TextBox 它的TextBox為 {Binding Intelligence,Mode=TwoWay} ,注意這個Mode=TwoWay,這是一個雙向系結的意思,通過它可以實作UI的內容更新了會回傳到后臺的系結屬性上,(因為沒有系結TextChanged事件,所以輸入完成后需要丟失焦點才會有反應我按下了Tab鍵,這里需要使用Binding的一個屬性UpdateSourceTrigger.他的型別是一個列舉,目前先不延申去講),我們看到了沒有針對性的寫后臺的的代碼通過一個屬性,就完成了多處的使用和更新,而這個屬性是業務層的,所以可以通過這個值來干很多的事情,

接下來我們繼續看上面其他的代碼:
<TextBlock Text="{Binding Name,Mode=TwoWay}" MinWidth="120"/>
<TextBox MinWidth="120" x:Name="tb_inputName"/>
<Button Content="通過代碼修改系結值得屬性,修改Name為杜文龍" Click="AlertText_Click"/>
在這里TextBlock 的Text系結了后臺代碼的Person實體下的Name屬性,
Name為tb_inputName的TextBox通過后臺代碼也實作了系結Name,還是雙向系結,在cs檔案下和XAML檔案下使用{Binding }效果是一樣的,

我們把TextBlock和TextBox都系結了Person的Name屬性,我們又在Button下創建了Click事件,用來模擬修改Name屬性(我們目前沒有分層,也沒有學習Command 所以假設cs檔案是業務層),
在Click事件中我們修改了person物件的Name屬性為杜文龍,Name屬性通過系結關聯了textblock和textbox,所以我們沒有直接操作UI層,
當Name屬性變化時,對應系結的UI控制元件的值也發生了變化,因為雙向系結當TextBox的值變化時,Name也發生了變化,這樣就可以在業務層處理了,,

我們嘗試把雙向系結修改為單向系結:
XAML下寫法:

cs下寫法:

在嘗試修改TextBox并把焦點切換走,會發現其他系結Name值得控制元件的值并沒有變化,這章就講這么多拉,主要是嘗試培養資料驅動得思維,
Binding還支持多級路徑、省略Path等寫法,作為新手目前不推薦延申這些知識,因為主要先搞明白什么是資料驅動,如何使用資料驅動,在去考慮如何使用更高級的功能,
漏掉了一個在Binding中比較重要的知識點,RelativeSource. 使用RelativeSource物件指向源物件,用這個可以在當前元素的基礎上查找其他物件用于系結到源物件,
在實際使用Binding的程序中大部分時間Binding都放在了資料模板和控制元件模板中,(資料模板是控制元件模板用于定義控制元件的UI),
在模板中撰寫Binding時有時候無法直接拿到我們需要系結的資料物件,我們不能確定我們需要的Source物件叫什么,但是我們直到了我們需要使用的物件在UI布局上的相對關系,比如控制元件自己關聯了某個資料,關鍵自己某個層級的容器資料,這個時候我們的RelativeSource就派上了用場,我們使用RelativeSource首先要3個關鍵引數,
AncestorType=我們需要查找的型別,比如Grid
AncestorLevel= 我們需要向上查找幾級
Path=我們找到的元素需要系結的屬性,
這三個關鍵的引數配置完,我們就可以完成對RelativeSource的使用,
<Grid x:Name="G0" Margin="12" Background="Red"> <TextBlock Text="In this Grid0 container"/> <Grid x:Name="G1" Margin="12" Background="Blue"> <TextBlock Text="In this Grid1 container"/> <Grid x:Name="G2" Margin="12" Background="Yellow"> <TextBlock Text="In this Grid2 container"/> <Grid x:Name="G3" Margin="12" Background="Beige"> <StackPanel> <TextBlock Text="In this Grid3 container"/> <TextBlock Name="ces" Text="{Binding RelativeSource={RelativeSource AncestorType=Grid,AncestorLevel=1},Path=Name}"/> </StackPanel> </Grid> </Grid> </Grid> </Grid>
我們嵌套幾個Grid,并在每個嵌套的Grid中都放入了一行文本用來顯示自己所在的位置,設定了Margin使他有部分的重疊,可以更好的看到相互之間的層級關系,最內層使用一個TextBlock.在TextBlock的Text屬性上使用RelativeSource,通過修改AncestorLevel 來設定向上查找Grid的等級,我們設定為1.向外層查找第一個找到的Grid物件,并系結對應的Name,可以嘗試修改一下并且看一下效果,

我創建了一個C#相關的交流群,用于分享學習資料和討論問題,歡迎有興趣的小伙伴:QQ群:542633085
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/271806.html
標籤:WPF
上一篇:WPF教程八:如何更好的使用Application程式集資源
下一篇:【翻譯】WPF 中附加行為的介紹 Introduction to Attached Behaviors in WPF
