主頁 > .NET開發 > WPF之資料系結

WPF之資料系結

2021-02-16 11:13:04 .NET開發

目錄
  • Data Binding在WPF中的地位
  • Binding基礎
  • Binding的源與路徑
    • 把控制元件作為Binding源與Binding標記擴展
    • 控制Binding的方向及資料更新
    • Binding的路徑(Path)
      • 關聯在Binding源的屬性上
      • Binding的多級路徑
      • 使用索引器作為Path
      • 使用集合或者DataView作為Binding源
    • “沒有Path”的Binding
    • 為Binding 指定源(Source)的幾種方法
      • 沒有Source的Binding——使用DataContext作為Binding的源
      • 使用集合物件作為串列控制元件的ItemsSource
        • 使用DisplayMemberPath
        • 使用DataTemplate
      • 使用ADO.NET物件作為Binding的源
        • 使用ListBox顯示DataTable
        • 使用ListView顯示DataTable
      • 使用XML資料作為Binding源
        • 從XML檔案顯示線性集合
        • 從XAML代碼顯示樹形資料結構
      • 使用LINQ檢索結果作為Binding的源
      • 使用ObjectDataProvider物件作為Binding的源
      • 使用Binding的RelativeSource
  • Binding對資料的轉換與校驗
    • Binding的資料校驗
      • 只在Target被外部方法更新時校驗資料
      • 在Target被Source屬性更新時也校驗資料
      • 顯示校驗失敗的錯誤訊息
    • Binding的資料轉換
  • MultiBinding(多路Binding)
  • 參考資料

Windows GUI運行的機理是使用訊息(Mesage)來驅使程式向前運行,訊息的主要來源是用戶的操作(如單擊滑鼠、按下按鈕),訊息又會被Windows翻譯并送達目標程式然后被程式所處理,程式是被來自UI的事件(即封裝過的訊息)驅使問前的,簡稱“訊息驅動”或“事件驅動”,因為訊息和事件大都來自于UI,所以統稱它們為“UI驅動程式”,使用“UI驅動程式”開發程式是“為了GUI而GUI”、單純地為了實作程式的GUI化,已經背離了程式的本質一一資料加演算法,

Data Binding在WPF中的地位

一般情況下,應用程式會具有三層結構:

  • 資料存盤層:由資料庫和檔案系統構成;
  • 資料處理層:也叫邏輯層,與業務邏輯相關、用于加工處理資料的演算法都集中在這里;
  • 資料展示層:把加工后的資料通過可視的界面展示給用戶或者通過其他種類的介面展示給別的應用程式(界面和介面兩個詞在英文中均為interface),還需要收集用戶的操作、把它們反饋給邏輯層,

WPF作為一種專門的展示層技術,華麗的外觀和影片只是它的表層現象,WPF引入了Data Binding概念以及與之配套的Dependency Property系統和DataTemplate,幫助程式員把思維的重心固定在了邏輯層、讓展示層永遠處于邏輯層的從屬地位,

展示層與邏輯層的溝通使用Data Binding來實作,加工好的資料會自動送達用戶界面加以顯示,被用戶修改過的資料也會自動傳回邏輯層,一旦資料被加工好又會被送達用戶界面,用加工好的資料驅動程式的用戶界面以文字、圖形、影片等形式把資料顯示出來——這就是“資料驅動UI”,

經過Data Binding優化,所有與業務邏輯相關的演算法都處在資料邏輯層,邏輯層成為一個能夠獨立運轉的、完整的體系,而用戶界面層則不含任何代碼、完全依賴和從屬于資料邏輯層,

Binding基礎

Binding:音譯“系結”,英文中有“捆綁”、“關聯”和“鍵聯”的含義,Binding更注重表達它是一種像橋梁一樣的關聯關系

把Binding比作資料的橋梁,它的兩端分別是Binding的源(Source)目標(Target),資料從哪里來哪里就是源,Binding是架在中間的橋梁,Binding目標是資料要往哪兒去,一般情況下,Binding源是邏輯層的物件,Binding目標是UI層的控制元件物件

先創建一個Student類,類的實體作為資料源,代碼如下:

public class Student : INotifyPropertyChanged
{
    private string name;

    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            if (PropertyChanged != null)
            {
                this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name"));
            }
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

準備一個TextBox和Button,代碼如下:

<StackPanel Margin="0,0,2,0">
    <TextBox x:Name="textBoxName" BorderBrush="Black" Margin="5"/>
    <Button Content="Add Age"  Margin="5"  Click="Button_Click"/>
</StackPanel>

使用Binding把資料源和UI元素連接起來,C#代碼如下:

public partial class MainWindow : Window
{
    Student stu = null;
    public MainWindow()
    {
        InitializeComponent();
        //準備資料源
        stu = new Student();
        //準備Binding
        Binding binding = new Binding();
        binding.Source = stu;
        binding.Path = new PropertyPath("Name");

        //使用Binding 連接資料源與Binding目標
        BindingOperations.SetBinding(this.textBoxName,TextBox.TextProperty,binding);
        //或
        this.textBoxName.SetBinding(TextBox.TextProperty, binding);
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        stu.Name += "Name";            
    }
}

上面這段代碼可以簡化成這樣:

public partial class MainWindow : Window
{
    Student stu = null;
    public MainWindow()
    {
        InitializeComponent();
        //三合一操作
        this.textBoxName.SetBinding(TextBox.TextProperty,new Binding("Name") { Source = stu = new Student()});
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        stu.Name += "Name";            
    }
}

UI上的元素關心的是哪個屬性值的變化,這個屬性就稱為Binding的路徑(Path),Binding是一種自動機制,當值變化后屬性要有能力通知Binding,讓Binding把變化傳遞給UI元素,需要在屬性的set 陳述句中激發一個PropertyChanged事件,這個事件不需要自己宣告,讓作為資料源的類實作System.ComponentModel名稱空間中的INotifyPropertyChanged介面,Binding會自動偵聽來自這個介面的PropertyChanged事件,

Binding模型如下所示:

Binding的源與路徑

Binding的源也就是資料的源頭,只要它是一個物件,并且通過屬性(Property)公開自己的資料,它就能作為Binding的源,如果想讓作為Binding源的物件具有自動通知Binding自己的屬性值已經變化的能力,那么就需要讓類實作INotifyPropertyChanged 介面并在屬性的set 陳述句中激發PropertyChanged事件,

除了使用這種物件作為資料源外,還有:

  • 把自己或自己的容器或子級元素當源
  • 用一個控制元件作為另一個控制元件的資料源
  • 把集合作為ItemsControl的資料源
  • 使用XML作為TreeView或Menu的資料源
  • 把多個控制元件關聯到一個“資料制高點”上,甚至干脆不給Binding 指定資料源、讓它自己去找

把控制元件作為Binding源與Binding標記擴展

為了讓UI元素產生一些聯動效果會使用Binding在控制元件間建立關聯,下面把一個TextBox的Text屬性關聯在Slider的Value屬性上,代碼如下:

<StackPanel>
    <TextBox x:Name="textBox1" Text="{Binding Path=Value, ElementName=slider1}" BorderBrush="Black" Margin="5"/>
    <Slider x:Name="slider1" Maximum="100" Minimum="0" Margin="5"/>
</StackPanel>

上面的代碼使用了Binding標記擴展語法:

<TextBox x:Name="textBox1" Text="{Binding Path=Value, ElementName=slider1}" BorderBrush="Black" Margin="5"/>

等效C#代碼如下:

this.textBox1.SetBinding(TextBox.TextProperty, new Binding("Value") { ElementName = "slider1" });
//一般也不會使用Binding的ElementName屬性,而是直接把物件賦值給Binding的Source屬性,方便重構
this.textBox1.SetBinding(TextBox.TextProperty, new Binding("Value") {Source=slider1});

Binding類的構造器本身可以接收Path作為引數,也常寫為:

<TextBox x:Name="textBox1" Text="{Binding Value, ElementName=slider1}" BorderBrush="Black" Margin="5"/>

控制Binding的方向及資料更新

控制Binding資料流向的屬性是Mode,它的型別是BindingMode列舉,可取值為:

  • TwoWay:導致更改源屬性或目標屬性時自動更新另一方,雙向模式
  • OneWay:在更改系結源(源)時更新系結目標(目標),從源向目標的單向溝通
  • OnTime:在應用程式啟動或資料背景關系更改時,更新系結目標,只在Binding關系確立時讀取一次資料
  • OneWayToSource:在目標屬性更改時,更新源屬性,從目標向源的單向溝通
  • Default:使用系結目標的默認 Mode 值,Binding的模式會根據目標的實際情況來確定(若是可編輯的就采用雙向模式,如TextBox.Text屬性;若是只讀的則采用單向模式,如TextBlock.Text),

控制Binding資料更新的屬性是UpdateSourceTrigger,它的型別是UpdateSourceTrigger列舉,可取值為:

  • PropertyChanged:每當系結目標屬性發生更改時,都會更新系結源,
  • LostFocus:每當系結目標元素失去焦點時,都會更新系結源,
  • Explicit:僅在呼叫 UpdateSource() 方法時更新系結源,
  • Default:系結目標屬性的默認 UpdateSourceTrigger 值,大多數依賴屬性的默認值為PropertyChanged,而Text屬性的默認值為LostFocus

Binding還具有NotifyOnSourceUpdatedNotifyOnTargetUpdated兩個bool型別的屬性,如果設為true,則當源或目標被更新后Binding會激發相應的SourceUpdated事件和TargetUpdated事件,實際作業中,我們可以通過監聽這兩個事件來找出有哪些資料或控制元件被更新了,

Binding的路徑(Path)

作為Binding源的物件可能有很多屬性,需要由Binding的Path屬性來指定Binding到底需要關注哪個屬性的值,如前面把Slider控制元件物件當作源、把它的Value屬性作為路徑,盡管在XAML代碼中或者Binding類的構造器引數串列中以一個字串來表示Path,但Path的實際型別是PropertyPath

關聯在Binding源的屬性上

XAML語法如下:

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

等效的C#代碼如下:

Binding binding = new Binding(){ Path = new PropertyPath("Value"), Source = this.slider1 }; 
this.textBox1.SetBinding(TextBox.TextProperty, binding);

使用Binding的構造器簡寫為:

Binding binding = new Binding() { Path = new PropertyPath("Value"), Source = this.slider1 };
this.textBox1.SetBinding(TextBox.TextProperty, binding);

Binding的多級路徑

Binding支持多級路徑(通俗地講就是一路“點”下去),如讓一個TextBox顯示另外一個TextBox的文本長度,XAML代碼如下:

<StackPanel>
    <TextBox x:Name="textBox1" BorderBrush="Black" Margin="5"/>
    <TextBox x:Name="textBox2" Text="{ Binding Path=Text.Length, ElementName=textBox1, Mode=OneWay}" BorderBrush="Black" Margin="5"/>
</StackPanel>

等效的C#代碼是:

this.textBox2.SetBinding(TextBox.TextProperty, new Binding("Text.Length")
{
    Source = this.textBox1,
    Mode =BindingMode.OneWay
});

效果如下:

使用索引器作為Path

集合型別的索引器(Indexer)又稱為帶參屬性,也能作為Path來使用,如讓一個TextBox顯示另一個TextBox文本的第四個字符,XAML代碼如下:

<!--XAML編輯器會認為這是錯誤的語法,但不影響編譯-->
<TextBox x:Name="textBox2" Text="{Binding Path=Text.[3],ElementName=textBox1,Mode=OneWay}" BorderBrush="Black" Margin="5"/>

等效的C#代碼是:

//可以把Text與[3]之間的那個“.”省略掉,一樣可以正確作業
this.textBox2.SetBinding(TextBox.TextProperty, new Binding("Text.[3]")
{
    Source = this.textBox1,
    Mode =BindingMode.OneWay
});

效果如下:

使用集合或者DataView作為Binding源

當使用一個集合或者DataView作為Binding源時,如果想把它的默認元素當作Path使用,則需要使用這樣的語法:

List<string> stringList = new List<string>() { "Tim", "Tom", "Blog" };
textBox1.SetBinding(TextBox.TextProperty, new Binding("/") { Source = stringList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("/Length") { Source = stringList, Mode = BindingMode.OneWay });
textBox3.SetBinding(TextBox.TextProperty, new Binding("/[2]") { Source = stringList, Mode = BindingMode.OneWay });

//等效代碼
textBox1.SetBinding(TextBox.TextProperty, new Binding("[0]") { Source = stringList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("[0].Length") { Source = stringList, Mode = BindingMode.OneWay });
textBox3.SetBinding(TextBox.TextProperty, new Binding("[0].[2]") { Source = stringList, Mode = BindingMode.OneWay });

效果如下:

如果集合元素的屬性仍然還是一個集合,想把子級集合中的元素當做Path,則可以使用多級斜線的語法(即一路“斜線”下去),代碼如下:

//相關型別
class City
{
    public string Name { get; set; }
}
class Province
{
    public string Name { get; set; }
    public List<City> CityList { get; set; }
}
class Country
{
    public string Name { get; set; }
    public List<Province> ProvinceList { get; set; }
}


//Binding
List<Country> countryList = new List<Country> 
{ 
    new Country() 
    { 
        Name = "中國", 
        ProvinceList = new List<Province>() 
        { 
            new Province() 
            { 
                Name = "四川", 
                CityList = new List<City>() 
                { 
                    new City(){ Name = "成都" }
                } 
            } 
        } 
    } 
};
textBox1.SetBinding(TextBox.TextProperty, new Binding("/Name") { Source = countryList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("/ProvinceList/Name") { Source = countryList });
textBox3.SetBinding(TextBox.TextProperty, new Binding("/ProvinceList/CityList/Name") { Source = countryList });
//等效C#代碼
textBox1.SetBinding(TextBox.TextProperty, new Binding("[0].Name") { Source = countryList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("[0].ProvinceList[0].Name") { Source = countryList });
textBox3.SetBinding(TextBox.TextProperty, new Binding("[0].ProvinceList[0].CityList[0].Name") { Source = countryList });

效果如下:

“沒有Path”的Binding

有時候在代碼中看到一些Path是一個“.”或者干脆沒有Path的Binding,這是一種比較特殊的情況——Binding源本身就是資料且不需要Path來指明,如string、int等基本型別的實體本身就是資料,無法指出通過它的哪個屬性來訪問這個資料,這時只需將Path的值設定為“.”就可以了,
注:在XAML代碼里這個“.”可以省略不寫,但在C#代碼里卻不能省略
代碼如下:

<StackPanel>
    <StackPanel.Resources>
        <sys:String x:Key="myString">
            菩提本無樹,明鏡亦非臺,
            本來無一物,何處惹塵埃,
        </sys:String>
    </StackPanel.Resources>
    <TextBlock x:Name="textBlock1" TextWrapping="Wrap" Margin="5" FontSize="16" Text="{Binding Path=.,Source={StaticResource ResourceKey=myString}}"/>
</StackPanel>

效果如下:

上面的代碼可以簡寫成以下形式:

<--形式1-!>
Text="{Binding .,Source={StaticResource ResourceKey=myString}}"
<--形式2-!>
Text="{Binding Source={StaticResource ResourceKey=myString}}"

等效的C#代碼如下:

string myString = "菩提本無樹,明鏡亦非臺,本來無一物,何處惹塵埃,";
//形式1
textBlock1.SetBinding(TextBlock.TextProperty, new Binding(".") { Source = myString });
//形式2,容易被誤解為沒有指定Path
textBlock1.SetBinding(TextBlock.TextProperty, new Binding() { Source = myString });

為Binding 指定源(Source)的幾種方法

Binding的源是資料的來源,只要一個物件包含資料并能通過屬性把資料暴露出來,它就能當作Binding的源來使用,常見的辦法有:

  • 把普通CLR型別單個物件指定為Source:包括.NET Framework自帶型別的物件和用戶自定義型別的物件(需實作INotifyPropertyChanged介面),方法有兩種——把物件賦值給Binding.Source屬性或把物件的Name賦值給Binding.ElementName,
  • 把普通CLR集合型別物件指定為Source:包括陣列、ListObservableCollection等集合型別(一般是把ItemsControl派生類控制元件的ItemsSource 屬性關聯到一個集合物件上),
  • 把ADO.NET資料物件指定為Source:包括DataTableDataView等物件,
  • 使用XmlDataProvider把XML資料指定為Source:XML可以用來表示單個資料物件或者集合,可以把樹狀結構的XML資料作為源指定給與級聯式的WPF控制元件(如TreeView和Menu)關聯的Binding,
  • 把依賴物件(Dependency Object)指定為Source:依賴物件可以作為Binding的目標或源,有可能形成Binding鏈,依賴物件中的依賴屬性可以作為Binding的Path
  • 把容器的DataContext 指定為Source(WPF Data Binding的默認行為):建立一個只設定Path不設定Source的Binding(只確定屬性、不確定物件),Binding會自動把控制元件的DataContext當作自己的Source(沿著控制元件樹向外層尋找帶有Path指定屬性的物件),
  • 通過ElementName 指定Source:在C#代碼里可以直接把物件作為Source賦值給Binding,但XAML無法訪問物件,所以只能使用物件的Name屬性來找到物件
  • 通過Binding的RelativeSource屬性相對地指定Source:當控制元件需要關注自己的、自己容器的或者自己內部元素的某個值就需要使用這種辦法,
  • 把ObjectDataProvider物件指定為Source:當資料源的資料不是通過屬性而是通過方法暴露給外界的時候,可以使用這兩種物件來包裝資料源再把它們指定為Source,
  • 把使用LINQ檢索得到的資料物件作為Binding的源:LINQ查詢的結果是一個IEnumerable型別物件

沒有Source的Binding——使用DataContext作為Binding的源

DataContext屬性被定義在FrameworkElement類(WPF控制元件的基類)里,所有WPF控制元件(包括容器控制元件)都具備這個屬性,WPF的UI布局是樹形結構,每個結點都是控制元件——在UI元素樹的每個結點都有DataContext,當一個Binding只知道自己的Path而不知道自己的Soruce時,它會沿著UI元素樹一路向樹的根部找過去,每路過一個結點就要看看這個結點的DataContext是否具有Path所指定的屬性:

  • 如果有,那就把這個物件作為自己的Source;
  • 如果沒有,那就繼續找下去;
  • 如果到了樹的根部還沒有找到,那這個Binding就沒有Source,因而也不會得到資料,

下面實作一個簡單的例子,代碼如下:

//創建一個名為Student的類,具有Id、Name、Age三個屬性
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}
<StackPanel Background="LightBlue">
    <StackPanel.DataContext>
        <local:Student Id="6" Age="29" Name="Tim"/>
    </StackPanel.DataContext>
    <Grid>
        <StackPanel>
            <TextBox Text="{Binding Path=Id}" Margin="5"/>
            <TextBox Text="{Binding Path=Name}" Margin="5"/>
            <TextBox Text="{Binding Path=Age}" Margin="5"/>
        </StackPanel>
    </Grid>
</StackPanel>

UI布局的樹狀圖如下:

三個TextBox的Text通過Binding獲取值,但只為Binding指定了Path,沒有指定Source,可以簡寫為:

<TextBox Text="{Binding Id}" Margin="5"/>
<TextBox Text="{Binding Name}" Margin="5"/>
<TextBox Text="{Binding Age}" Margin="5"/>

效果如下:

當某個DataContext是一個簡單型別物件的時候,Path可以設定為“.”或者省略不寫,可能看到一個“既沒有Path又沒有Source的”Binding:

<StackPanel>
    <StackPanel.DataContext>
        <sys:String>Hello DataContext!</sys:String>
    </StackPanel.DataContext>
    <Grid>
        <StackPanel>
            <TextBlock Text="{Binding}" Margin="5"/>
            <TextBlock Text="{Binding}" Margin="5"/>
            <TextBlock Text="{Binding}" Margin="5"/>
        </StackPanel>
    </Grid>
</StackPanel>

DataContext是一個“依賴屬性”,當沒有為控制元件的某個依賴屬性顯式賦值時,控制元件會把自己容器的屬性值“借過來”當作自己的屬性值,“Binding沿著UI元素樹向上找”只是一個錯覺,實際上是屬性值沿著UI元素樹向下傳遞了

在實際作業中DataContext的用法是非常靈活的,比如:

  • 當UI上的多個控制元件都是用Binding關注同一個物件時,不妨使用DataContext,
  • 當作為Source的物件不能被直接訪問的時候——比如B表單內的控制元件想把A表單內的控制元件(private訪問級別)當作自己的Binding源時,可以把這個控制元件(或者控制元件的值)作為表單A的DataContext(public訪問級別)從而暴露資料,

使用集合物件作為串列控制元件的ItemsSource

WPF中的串列式控制元件們派生自ItemsControl類,繼承了ItemsSource屬性,ItemsSource屬性可以接收一個IEnumerable介面派生類的實體作為自己的值(所有可被迭代遍歷的集合都實作了這個介面,包括陣列、List等),每一個ItemsControl的派生類都具有自己對應的Item Container(條目容器,如ListBox的ListBoxItem、ComboBox的ComboBoxItem),只要為一個ItemsControl物件設定了ItemsSource屬性值,ItemsControl物件就會自動迭代其中的資料元素,為每個資料元素準備一個條目容器,并使用Binding在條目容器與資料元素之間建立起關聯,

使用DisplayMemberPath

XAML代碼如下:

<StackPanel>
    <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>

C#代碼如下:

//準備資料源
List<Student> stuList = new List<Student>()
{
    new Student(){Id=0,Name="Tim",Age=29},
    new Student(){Id=1,Name="Tom",Age=28},
    new Student(){Id=2,Name="Kyle",Age=27},
    new Student(){Id=3,Name="Tony",Age=26},
    new Student(){Id=4,Name="Vina",Age=25},
    new Student(){Id=5,Name="Mike",Age=24},
};

//為ListBox設定Binding
listBoxStudents.ItemsSource = stuList;
listBoxStudents.DisplayMemberPath = "Name";

//為TextBox設定Binding
Binding binding = new Binding("SelectedItem.Id") { Source = listBoxStudents };
textBoxId.SetBinding(TextBox.TextProperty, binding);

效果如下:

當DisplayMember屬性被賦值后,ListBox在獲得ItemsSource的時候就會創建等量的ListBoxItem并以DisplayMemberPath屬性值為Path創建Binding,Binding的目標是ListBoxItem的內容插件(實際上是一個TextBox),這個創建Binding的程序是在DisplayMemberTemplateSelector類的SelectTemplate方法里完成的,方法定義的格式如下:

public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
    //...
    FrameworkElementFactory text = ContentPresenter.CreateTextBlockFactory();
    Binding binding = new Binding();
    binding.XPath = _displayMemberPath;
    binding.StringFormat = _stringFormat;
    text.SetBinding(TextBlock.TextProperty, binding);
    //...
}

使用DataTemplate

洗掉C#代碼中的listBoxStudents.DisplayMemberPath = "Name";,XAML代碼如下:

<StackPanel>
    <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="150" Margin="5">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Path=Id}" Width="30"/>
                    <TextBlock Text="{Binding Path=Name}" Width="60"/>
                    <TextBlock Text="{Binding Path=Age}" Width="30"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</StackPanel>

效果如下:

使用集合型別作為串列控制元件的ItemsSource時一般會考慮使用ObservableCollection代替List,因為ObservableCollection類實作了INotifyCollectionChanged和INotifyPropertyChanged介面,能把集合的變化立刻通知顯示它的串列控制元件,改變會立刻顯現出來,

使用ADO.NET物件作為Binding的源

在.NET開發中使用ADO.NET類對資料庫進行操作,常見的作業是從資料庫中把資料讀取到DataTable中,再把DataTable顯示在UI串列控制元件里(如成績單、博客文章串列、論壇帖子串列等),在流行的軟體架構中并不把直接顯示DataTable,而是先通過LINQ等手段把DataTable里的資料轉換成恰當的用戶自定義型別集合,但WPF也支持在串列控制元件與DataTable之間直接建立Binding,
假設已有一個DataTable的實體,資料內容如表所示:

Id Name Age
1 Tim 29
2 Tom 28
3 Tony 27
4 Kyle 26
5 Vina 25
6 Emily 24

加載方法如下所示:

private DataTable Load()
{
    DataTable dt = new DataTable();
    dt.Columns.Add("Id");
    dt.Columns.Add("Name");
    dt.Columns.Add("Age");
    dt.Rows.Add("1", "Tim", "29");
    dt.Rows.Add("2", "Tom", "28");
    dt.Rows.Add("3", "Tony", "27");
    dt.Rows.Add("4", "Kyle", "26");
    dt.Rows.Add("5", "Vina", "25");
    dt.Rows.Add("6", "Emily", "24");
    return dt;
}

使用ListBox顯示DataTable

XAML代碼如下:

<StackPanel Background="LightBlue">
    <ListBox x:Name="listBoxStudents" Height="130" Margin="5"/>
    <Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>

C#代碼如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    //獲取DataTable實體
    DataTable dt = Load();

    listBoxStudents.DisplayMemberPath = "Name";
    listBoxStudents.ItemsSource = dt.DefaultView;
}

效果如下:

最重要的代碼是listBoxStudents.ItemsSource = dt.DefaultView;,DataTable的DefaultView屬性是一個DataView型別的物件(實作了IEnumerable介面),可以被賦值給ListBox.ItemsSource屬性,

使用ListView顯示DataTable

多數情況下會選擇ListView控制元件來顯示一個DataTable,XAML代碼如下:

<StackPanel Background="LightBlue">
    <ListView x:Name="listViewStudents" Height="200" Margin="5">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}"/>
                <GridViewColumn Header="Name" Width="80" DisplayMemberBinding="{Binding Name}"/>
                <GridViewColumn Header="Age" Width="60" DisplayMemberBinding="{Binding Age}"/>
            </GridView>
        </ListView.View>
    </ListView>
    <Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>

C#代碼如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    //獲取DataTable實體
    DataTable dt = Load();

    listViewStudents.ItemsSource = dt.DefaultView;  
}

效果如下:

有幾點需要注意的地方:

  • ListView和GridView不是同一級別的控制元件,ListView是ListBox的派生類,而GridView是ViewBase的派生類,ListView的View屬性是一個ViewBase型別的物件,GridView可以作為ListView的View來使用,而不能當作獨立的控制元件來使用,目前,ListView的View只有一個GridView可用,估計微軟在這里還會有擴展,
  • GridView的內容屬性是Columns(GridViewColumnCollection型別物件),此處省略了<GridView.Columns>...</GridView.Columns>這層標簽(XAML支持對內容屬性的簡寫),直接在的內容部分定義了三個GridViewColumn物件,
  • GridViewColumn物件最重要的一個屬性是DisplayMemberBinding(型別為BindingBase),可以指定這一列使用什么樣的Binding去關聯資料(與ListBox不同,ListBox使用的是DisplayMemberPath屬性),
  • 如果想用更復雜的結構來表示這一列的標題(Header)或資料,則可以為GridViewColumn設定HeaderTemplate和CellTemplate屬性,它們的型別都是DataTemplate,

DataTable不能直接拿來為ItemsSource賦值,不過可以把DataTable物件放在一個物件的DataContext屬性里,并且為ItemsSource設定一個既無Path又無Source的Binding,Binding能自動找到它的DefaultView并當作自己的Source來使用:

private void Button_Click(object sender, RoutedEventArgs e)
{
    //獲取DataTable實體
    DataTable dt = Load();

    listViewStudents.DataContext = dt;
    listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
}

使用XML資料作為Binding源

.NET Framework提供了兩套處理XML資料的類別庫:

  • 符合DOM(Document Object Model,檔案物件模型)標準的類別庫:包括XmlDocument、XmlElement、XmlNode、XmlAttribute等類,特點是中規中矩、功能強大、但背負了太多XML的傳統和復雜,
  • 以LINQ(Language-Integrated Query,語言集成查詢)為基礎的類別庫:包括XDocument、XElement、XNode、XAttribute等類,特點是可以使用LINQ進行查詢和操作、方便快捷、但會產生很多臨時物件
    本小節主要講解基于DOM標準的XML類別庫,基于LINQ的部分放在下一節里討論,

現代程式設計的大多數資料傳輸都基于SOAP(Simple Object Access Protocol,簡單物件訪問協議)相關的協議,而SOAP又是通過將物件序列化為XML文本進行傳輸,XML文本是樹形結構的,所以XML可以方便地用于表示線性集合(如Array、List等)和樹形結構資料**,

當使用XML資料作為Binding的Source時將使用XPath屬性而不是Path屬性來指定資料的來源,XPath作為XML語言的功能有著一整套語法,詳盡的講解可以查閱:

  • XPath 語法
  • XML Path 語言 (XPath)

從XML檔案顯示線性集合

下面的XML文本是一組學生的資訊(假設存放在D:\RawData.xml檔案中),內容如下:

<?xml version="1.0" encoding="utf-8" ?>
<StudentList>
  <Student Id="1">
    <Name>Tim</Name>
  </Student>
  <Student Id="2">
    <Name>Tom</Name>
  </Student>
  <Student Id="3">
    <Name>Vina</Name>
  </Student>
  <Student Id="4">
    <Name>Emily</Name>
  </Student>
</StudentList>

把它顯示在一個ListView控制元件里,XAML代碼如下:

<StackPanel Background="LightBlue">
    <ListView x:Name="listViewStudents" Height="200" Margin="5">
        <ListView.View>
            <GridView>
                <GridView.Columns>
                    <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding XPath=@Id}"/>
                    <GridViewColumn Header="Name" Width="80" DisplayMemberBinding="{Binding XPath=Name}"/>
                </GridView.Columns>
            </GridView>
        </ListView.View>
    </ListView>
    <Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>

Button的Click事件處理器中可以通過XmlDataProviderDocumentSource屬性指定XML,代碼如下:

//第一種:手動加載XML檔案并賦值給XmlDataProvider的Document屬性
private void Button_Click(object sender, RoutedEventArgs e)
{
    XmlDocument doc = new XmlDocument();
    doc.Load(@"D:\RawData.xml");

    XmlDataProvider xdp = new XmlDataProvider();
    xdp.Document = doc;

    //使用XPath選擇需要暴露的資料
    //現在是需要暴露一組Student
    xdp.XPath = @"/StudentList/Student";

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

//第二種:直接指定XML檔案所在的位置(無論XML檔案存盤在本地硬碟還是網路上)給XmlDataProvider的Source屬性
private void Button_Click(object sender, RoutedEventArgs e)
{
    XmlDataProvider xdp = new XmlDataProvider();
    xdp.Source = new Uri(@"D:\RawData.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,不加@符號的字串表示的是子級元素,

從XAML代碼顯示樹形資料結構

XAML代碼如下:

<Window.Resources>
    <XmlDataProvider x:Key="xdp" XPath="FileSystem/Folder">
        <x:XData>
            <FileSystem xmlns="">
                <Folder Name="Books">
                    <Folder Name="Programming">
                        <Folder Name="Windows">
                            <Folder Name="WPF"/>
                            <Folder Name="MFC"/>
                            <Folder Name="Delphi"/>
                        </Folder>
                    </Folder>
                    <Folder Name="Tools">
                        <Folder Name="Development"/>
                        <Folder Name="Designment"/>
                        <Folder Name="Players"/>
                    </Folder>
                </Folder>
            </FileSystem>
        </x:XData>
    </XmlDataProvider>
</Window.Resources>

<Grid>
    <TreeView ItemsSource="{Binding Source={StaticResource xdp}}">
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding XPath=Folder}">
                <TextBlock Text="{Binding XPath=@Name}"/>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
</Grid>

效果如下:

如果把XmlDataProvider直接寫在XAML代碼里,它的XML資料需要放在<x:XData>...</x:XData>標簽里,StaticResourceHierarchicalDataTemplate等學完ResourceTemplate才會明白,

使用LINQ檢索結果作為Binding的源

.NET Framework 3.0開始支持LINQ(Language-Integrated Query,語言集成查詢),使用LINQ可以方便地操作集合物件、DataTable物件和XML物件,不必動輒就把好幾層foreach回圈嵌套,
LINQ查詢的結果是一個IEnumerable型別物件,而IEnumerable又派生自IEnumerable,所以它可以作為串列控制元件的ItemsSource來使用,

創建了一個名為Student的類:

public class Student

{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

XAML代碼如下:

<StackPanel Background="LightBlue">
    <ListView x:Name="listViewStudents" Height="145" Margin="5">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}"/>
                <GridViewColumn Header="Name" Width="100" DisplayMemberBinding="{Binding Name}"/>
                <GridViewColumn Header="Age" Width="80" DisplayMemberBinding="{Binding Age}"/>
            </GridView>
        </ListView.View>
    </ListView>
    <Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>

從一個已經填充好的List物件中檢索出所有名字以字母T開頭的學生,代碼如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    List<Student> stuList = new List<Student>()
    {
        new Student(){Id=0,Name="Tim",Age=29},
        new Student(){Id=1,Name="Tom",Age=28},
        new Student(){Id=2,Name="Kyle",Age=27},
        new Student(){Id=3,Name="Tony",Age=26},
        new Student(){Id=4,Name="Vina",Age=25},
        new Student(){Id=5,Name="Mike",Age=24}
    };

    listViewStudents.ItemsSource = from stu in stuList 
                                   where stu.Name.StartsWith("T") 
                                   select stu;
}

從一個已經填充好的DataTable物件中檢索出所有名字以字母T開頭的學生,代碼如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    DataTable dt = this.GetDataTable();
    listViewStudents.ItemsSource =
        from row in dt.Rows.Cast<DataRow>()
        where Convert.ToString(row["Name"]).StartsWith("T")
        select new Student()
        {
            Id = int.Parse(row["Id"].ToString()),
            Name = row["Name"].ToString(),
            Age = int.Parse(row["Age"].ToString())
        };
}

private DataTable GetDataTable()
{
    DataTable dt = new DataTable();            
    dt.Columns.Add("Id");
    dt.Columns.Add("Name");
    dt.Columns.Add("Age");
    dt.Rows.Add("1", "Tim", "29");
    dt.Rows.Add("2", "Tom", "28");
    dt.Rows.Add("3", "Tony", "27");
    dt.Rows.Add("4", "Kyle", "26");
    dt.Rows.Add("5", "Vina", "25");
    dt.Rows.Add("6", "Emily", "24");
    return dt;
}

從一個存盤資料的XML檔案(D:RawData.xml)中檢索出所有名字以字母T開頭的學生,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<StudentList>
  <Class>
    <Student Id="0" Name="Tim" Age="29"/>
    <Student Id="1" Name="Tom" Age="28"/>
    <Student Id="2" Name="Mess" Age="27"/>
  </Class>
  <Class>
    <Student Id="3" Name="Tony" Age="26"/>
    <Student Id="4" Name="Vina" Age="25"/>
    <Student Id="5" Name="Emily" Age="24"/>
  </Class>
</StudentList>
private void Button_Click(object sender, RoutedEventArgs e)
{
    XDocument xdoc = XDocument.Load(@"D:\RawData.xml");

    listViewStudents.ItemsSource =
        from element in xdoc.Descendants("Student")
        where element.Attribute("Name").Value.StartsWith("T")
        select new Student()
        {
            Id = int.Parse(element.Attribute("Id").Value),
            Name = element.Attribute("Name").Value,
            Age = int.Parse(element.Attribute("Age").Value)
        };
}

注:xdoc.Descendants("Student")這個方法可以跨越XML的層級,

效果如下:

使用ObjectDataProvider物件作為Binding的源

有時很難保證一個類的所有資料都使用屬性暴露出來,比如需要的資料可能是方法的回傳值,重新設計底層類的風險和成本會比較高且黑盒參考類別庫時不可能改變已經編譯好的類,這時就需要使用ObjectDataProvider來包裝作為Binding源的資料物件了,

ObjectDataProvider把物件作為資料源提供給Binding,之前的XmlDataProvider也是把XML資料作為資料源提供給Binding,兩者的父類都是DataSourceProvider抽象類

有一個名為Calculator的類,它具有計算加、減、乘、除的方法:

class Calculator
{
    //加法
    public string Add(string arg1, string arg2)
    {
        double x = 0;
        double y = 0;
        double z = 0;
        if (double.TryParse(arg1, out x) && double.TryParse(arg2, out y))
        {
            z = x + y;
            return z.ToString();
        }
        return "Input Error!";
    }

    //其他演算法...
}

先隨便新建一個WPF專案,添加一個Button,Button的Click事件處理器如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    ObjectDataProvider odp = new ObjectDataProvider();
    odp.ObjectInstance = new Calculator();
    odp.MethodName = "Add";
    odp.MethodParameters.Add("100");
    odp.MethodParameters.Add("200");
    MessageBox.Show(odp.Data.ToString());
}

效果如下:

由上可知ObjectDataProvider物件與被它包裝的物件關系如下所示:

把ObjectDataProvider當作Binding的Source來使用,實作第三個TextBox能實時地顯示上面兩個TextBox輸入數字的和,代碼和截圖如下:

<StackPanel Background="LightBlue">
    <TextBox x:Name="textBoxArg1" Margin="5"/>
    <TextBox x:Name="textBoxArg2" Margin="5"/>
    <TextBox x:Name="textBoxResult" Margin="5"/>
</StackPanel>
//創建并配置ObjectDataProvider物件
ObjectDataProvider odp = new ObjectDataProvider();
odp.ObjectInstance = new Calculator();
odp.MethodName = "Add";
odp.MethodParameters.Add("0");
odp.MethodParameters.Add("0");

//以ObjectDataProvider物件為Source創建Binding,Path是ObjectDataProvider物件MethodParameters屬性所參考的集合中的元素
//Binding物件只負責把從UI收集到的資料寫入其直接Source(即ObjectDataProvider物件)而不是被ObjectDataProvider物件包裝著的Calculator物件
//UpdataSourceTrigger屬性設定為一有更新立刻將值傳回Source
Binding bindingToArg1 = new Binding("MethodParameters[0]")
{
    Source = odp,
    BindsDirectlyToSource = true,
    UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};

Binding bindingToArg2 = new Binding("MethodParameters[1]")
{
    Source = odp,
    BindsDirectlyToSource = true,
    UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};

//以ObjectDataProvider物件為Source創建Binding,ObjectDataProvider物件本身就代表了資料,Path使用的是.而非其Data屬性
Binding bindingToResult = new Binding(".") { Source = odp };

//將Binding關聯到UI元素上
textBoxArg1.SetBinding(TextBox.TextProperty, bindingToArg1);
textBoxArg2.SetBinding(TextBox.TextProperty, bindingToArg2);
textBoxResult.SetBinding(TextBox.TextProperty, bindingToResult);

ObjectDataProvider類的作用是用來包裝一個以方法暴露資料的物件,這里先創建了一個ObjectDataProvider物件,然后用一個Calculator物件為其ObjectInstance屬性賦值,就把一個Calculator物件包裝在了ObjectDataProvider物件里,

還有另一種辦法創建被包裝的物件,告訴ObjectDataProvider將被包裝物件的型別和希望呼叫的構造器,讓ObjectDataProvider自己去創建被包裝物件,一般會在XAML代碼中使用這種指定型別和構造器的辦法,代碼如下:

//...
odp.ObjectType = typeof(YourClass);
odp.ConstructorParameters.Add(arg1);
odp.ConstructorParameters.Add(arg2);
//...

多載方法的區別在于引數串列,上面代碼中向MethodParameters屬性中加入了兩個string型別的物件,相當于告訴ObjectDataProvider物件去呼叫Calculator物件中具有兩個string型別引數的Add方法(MethodParameters屬性是型別敏感的),

三個TextBox都以ObjectDataProvider物件為資料源,只是前兩個TextBox在Binding的資料流向上做了限制,原因如下:

  • ObjectDataProvider的MethodParameters不是依賴屬性,不能作為Binding的目標
  • 資料驅動UI的理念要求盡可能地使用資料物件作為Binding的Source而把UI元素當做Binding的Target

使用Binding的RelativeSource

有時候只知道Source物件與作為Binding目標的物件在UI布局上的相對關系,比如控制元件自己關聯自己的某個屬性、關聯自己某級容器的資料,此時要使用Binding的RelativeSource屬性,RelativeSource屬性的資料型別為RelativeSource類,通過這個類的幾個靜態或非靜態屬性可以控制它搜索相對資料源的方式,

RelativeSource類的非靜態屬性有3個:

  • AncestorLevel屬性:以Binding目標控制元件為起點的層級偏移量(如下面例子中d2的偏移量是1、g2的偏移量為2,依次類推),
  • AncestorType屬性:告訴Binding尋找哪個型別的物件作為自己的源,不是這個型別的物件會被跳過,
  • Mode屬性:是RelativeSourceMode列舉,取值有:PreviousData 、TemplatedParent 、Self 和FindAncestor,

RelativeSource類的靜態屬性3個:PreviousSelfTemplateParent,它們的型別是RelativeSource類,這3個靜態屬性是為了在XAML代碼里直接獲取RelativeSource實體,本質是創建一個RelativeSource實體、把實體的Mode屬性設定為相應的值,然后回傳這個實體

RelativeSource類的原始碼參考RelativeSource,靜態屬性的部分原始碼如下:

public static RelativeSource PreviousData
{
    get
    {
        if (s_previousData =https://www.cnblogs.com/timefiles/p/= null)
        {
            s_previousData = new RelativeSource(RelativeSourceMode.PreviousData);
        }

        return s_previousData;
    }
}

下面舉例說明,在多層布局控制元件內放置一個TextBox,XAML代碼如下:

<Grid x:Name="g1" Background="Red" Margin="10">
    <DockPanel x:Name="d1" Background="Orange" Margin="10">
        <Grid x:Name="g2" Background="Yellow" Margin="10">
            <DockPanel x:Name="d2" Background="LawnGreen" Margin="10">
                <TextBox x:Name="textBox1" FontSize="24" Margin="10"/>
            </DockPanel>
        </Grid>
    </DockPanel>
</Grid>

從Binding的第一層依此向外找,找到第一個Grid型別物件后把它當做自己的源,C#代碼、等效XAML、效果截圖如下:

RelativeSource rs = new RelativeSource(RelativeSourceMode.FindAncestor);
rs.AncestorLevel = 1;
rs.AncestorType = typeof(Grid);
Binding binding = new Binding("Name") { RelativeSource = rs };
textBox1.SetBinding(TextBox.TextProperty, binding);
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type Grid},AncestorLevel=1},Path=Name}"

從Binding的第一層依此向外找,找到第二個DockPanel型別物件后把它當做自己的源,C#代碼、等效XAML、效果截圖如下:

RelativeSource rs = new RelativeSource(RelativeSourceMode.FindAncestor);
rs.AncestorLevel = 2;
rs.AncestorType = typeof(DockPanel);
Binding binding = new Binding("Name") { RelativeSource = rs };
textBox1.SetBinding(TextBox.TextProperty, binding);
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DockPanel},AncestorLevel=2},Path=Name}"

TextBox關聯自身的Name屬性,C#代碼、等效XAML、效果截圖如下:

RelativeSource rs = new RelativeSource();
rs.Mode = RelativeSourceMode.Self;
Binding binding = new Binding("Name") { RelativeSource = rs };
textBox1.SetBinding(TextBox.TextProperty, binding);
<!--使用非靜態屬性-->
Text="{Binding RelativeSource={RelativeSource Mode=Self},Path=Name}"
<!--使用靜態屬性-->
Text="{Binding RelativeSource={ x:Static RelativeSource.Self} ,Path=Name}"

Binding對資料的轉換與校驗

Binding可以通過ValidationRules屬性對資料有效性進行校驗,通過Converter屬性為資料設定轉換器(兩端要求使用不同的資料型別時),

Binding的資料校驗

Binding的ValidationRules屬性型別是Collection可以為每個Binding設定多個資料校驗條件的,其中每一個條件是一個ValidationRule型別物件

ValidationRule類是個抽象類,在使用的時候需要創建它的派生類并實作它的Validate方法,對于Validate方法的回傳值(ValidationResult型別物件):

  • 校驗通過,則把回傳值的IsValid屬性設定為true,
  • 校驗失敗,則把回傳值IsValid屬性設定為false并為ErrorContent屬性設定一個合適的訊息內容(一般是個字串),

在UI上繪制一個TextBox和一個Slider,準備一個ValidationRule的派生類,代碼如下:

<StackPanel>
    <TextBox x:Name="textBox1" Margin="5"/>
    <Slider x:Name="slider1" Minimum="0" Maximum="100" Margin="5"/>
</StackPanel>
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 >= 00 && d <= 100)
            {
                return new ValidationResult(true, null);
            }
        }

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

在后臺C#代碼里使用Binding把Slider和TextBox關聯起來——以Slider為源、TextBox為目標,

只在Target被外部方法更新時校驗資料

在后臺C#代碼里使用Binding以Slider為源、TextBox為目標關聯起來,Slider的取值范圍是0到100,需要校驗TextBox里輸入的值是不是在0到100這個范圍內,C#代碼如下:

//建立Binding
Binding binding = new Binding("Value") { Source = slider1 };
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
RangeValidationRule rvr = new RangeValidationRule();
binding.ValidationRules.Add(rvr);
textBox1.SetBinding(TextBox.TextProperty, binding);

當輸入0到100之間的值時程式正常顯示,區間之外的值或不能被決議的值時TextBox會顯示紅色邊框,錯誤值不會傳遞給Source,效果如下所示:

在Target被Source屬性更新時也校驗資料

Binding默認只在Target被外部方法更新時校驗資料,而來自Binding的Source屬性更新Target時是不會進行校驗的,當來自Source的資料也有可能出問題時,需要將校驗條件的ValidatesOnTargetUpdated屬性設定為true

把slider1的取值范圍由0到100改成-10到110,當Slider的滑塊移出有效范圍時,TextBox也會顯示校驗失敗的效果,代碼、效果如下:

<Slider x:Name="slider1" Minimum="-10" Maximum="110" Margin="5"/>
//建立Binding
Binding binding = new Binding("Value") { Source = slider1 };
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
RangeValidationRule rvr = new RangeValidationRule();
rvr.ValidatesOnTargetUpdated = true;
binding.ValidationRules.Add(rvr);
textBox1.SetBinding(TextBox.TextProperty, binding);

顯示校驗失敗的錯誤訊息

當校驗錯誤的時候Validate方法回傳的ValidationResult物件攜帶著一條錯誤訊息,顯示錯誤訊息需要用到后面才會詳細講解的知識——路由事件(Routed Event)

在創建Binding時要把Binding物件的NotifyOnValidationError屬性設為true,這樣,當資料校驗失敗的時候Binding會像報警器一樣發出一個信號,這個信號會以Binding物件的Target為起點在UI元素樹上傳播,信號每到達一個結點,如果這個結點上設定有對這種信號的偵聽器(事件處理器),那么這個偵聽器就會被觸發用以處理這個信號,信號處理完后,程式員還可以選擇是讓信號繼續向下傳播還是就此終止——這就是路由事件,信號在UI元素樹上的傳遞程序就稱為路由(Route)

代碼、效果如下:

//建立Binding
Binding binding = new Binding("Value") { Source = slider1 };
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
RangeValidationRule rvr = new RangeValidationRule();
rvr.ValidatesOnTargetUpdated = true;
binding.ValidationRules.Add(rvr);
binding.NotifyOnValidationError = true;
textBox1.SetBinding(TextBox.TextProperty, binding);
textBox1.AddHandler(Validation.ErrorEvent, new RoutedEventHandler(ValidationError));

//偵聽器
private void ValidationError(object sender, RoutedEventArgs e)
{
    if (Validation.GetErrors(textBox1).Count > 0)
    {
        textBox1.ToolTip = Validation.GetErrors(textBox1)[0].ErrorContent.ToString();
    }
}

Binding的資料轉換

Binding有一種機制稱為資料轉換(Data Convert),當Source端Path所關聯的資料與Target端目標屬性資料型別不一致時(如Slider的Value屬性是double型別值、TextBox的Text屬性是string型別值),可以添加資料轉換器(Data Converter)

double型別與string型別互相轉換處理起來比較簡單,WPF類別庫自動替我們做了,但有些型別之間的轉換就不是WPF能替我們做的了,如下面這些情況:

  • Source里的資料是Y、N和X三個值(可能是char型別、string型別或自定義列舉型別),UI上對應的是CheckBox控制元件,需要把這三個值映射為它的IsChecked屬性值(bool?型別)
  • 當TextBox里已經輸入了文字時用于登陸的Button才會出現,這是string型別與Visibility列舉型別或bool型別之間的轉換(Binding的Mode將是OneWay),
  • Source里的資料可能是Male或Female(string或列舉),UI上對應的是用于顯示頭像的Image控制元件,這時候需要把Source里的值轉換成對應的頭像圖片URI(亦是OneWay),

以上情況,只能自己動手寫Converter,方法是創建一個類并讓這個類實作IValueConverter介面,IValueConverter介面定義如下:

public interface IValueConverter
{
    object Convert(object value, Type targetType, object parameter, CultureInfo culture);

    object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}

當資料從Binding的Source流向Target時,Converter方法將被呼叫;反之,ConvertBack方法將被呼叫,兩個方法的引數串列一模一樣:

  • 第一個引數為object,最大限度地保證了Converter的重用性(可以在方法體內對實際型別進行判斷);
  • 第二個引數用于確定方法的回傳型別,可以理解為outputType避免與Binding的Target混淆
  • 第三個引數用于把額外的資訊傳入方法,若需要傳遞多個資訊則可把資訊放入一個集合物件來傳入方法

Binding物件的Mode屬性為TwoWay或Default行為與TwoWay一致,則兩個方法都有可能被呼叫;Mode為OneWay或Default行為與OneWay一致則只有Convert方法會被呼叫,其他情況同理,

下面這個例子是一個Converter的綜合實體,程式的用途是在串列里向玩家顯示一些軍用飛機的狀態,
首先創建幾個自定義型別資料:

//種類
public enum Category
{
    Bomber,
    Fighter
}

//狀態
public enum State
{
    Available,
    Locked,
    Unknown
}

//飛機
public class Plane
{
    //Category屬性在UI里被映射為轟炸機或戰斗機的圖示,
    public Category Category { get; set; }
    public string Name { get; set; }
    //State屬性在UI里被映射為CheckBox
    public State State { get; set; }
}

轟炸機(Bomber.png)或戰斗機(Fighter.png)的圖示如下:

需要提供兩個Converter,一個是由Category型別單向轉換為string型別(XAML編譯器能夠把string物件決議為圖片資源),另一個是在State與bool?型別之間雙向轉換,代碼如下:

public class CategoryToSourceConverter : IValueConverter
{
    //將Category轉換為Uri
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Category c = (Category)value;
        switch (c)
        {
            case Category.Bomber:
                return @"\Icons\Bomber.png";
            case Category.Fighter:
                return @"\Icons\Fighter.png";
            default:
                return null;
        }
    }

    //不會被呼叫
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class StateToNullableBoolConverter : IValueConverter
{
    //將State轉換為bool?
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        State s = (State)value;
        switch (s)
        {
            case State.Available:
                return true;
            case State.Locked:
                return false;
            case State.Unknown:
            default:
                return null;
        }
    }

    //將bool?轉換為State
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        bool? nb = (bool?)value;
        switch (nb)
        {
            case true:
                return State.Available;
            case false:
                return State.Locked;
            case null:
            default:
                return State.Unknown;
        }
    }
}

在XAML代碼中,以資源的形式創建了兩個Converter的實體,為listBoxPlane添加用于顯示資料的DataTemplate,代碼如下:

<Window.Resources>
    <local:CategoryToSourceConverter  x:Key="cts"/>
    <local:StateToNullableBoolConverter  x:Key="stnb"/>
</Window.Resources>

<StackPanel Background="LightBlue">
    <ListBox x:Name="listBoxPlane" Height="160" Margin="5">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <Image Width="40" Height="40" Source="{Binding Path=Category,Converter={StaticResource cts}}"/>
                    <TextBlock Text="{Binding Path=Name}" Width="60" Margin="80,0"/>
                    <CheckBox IsThreeState="True" IsChecked="{Binding Path=State,Converter={StaticResource stnb}}"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <Button x:Name="buttonLoad" Content="Load" Height="25" Margin="5,0" Click="buttonLoad_Click"/>
    <Button x:Name="buttonSave" Content="Save" Height="25" Margin="5,0" Click="buttonSave_Click"/>
</StackPanel>

Load按鈕的Click事件處理器負責把一組飛機的資料賦值給ListBox的ItemsSource屬性,Save按鈕的Click事件處理器負責把用戶更改過的資料寫入檔案,代碼如下:

//Load按鈕Click事件處理器
private void buttonLoad_Click(object sender, RoutedEventArgs e)
{
    List<Plane> planeList = new List<Plane>()
    {
        new Plane(){Category=Category.Bomber,Name="B-1",State=State.Unknown},
        new Plane(){Category=Category.Bomber,Name="B-2",State=State.Unknown},
        new Plane(){Category=Category.Fighter,Name="F-22",State=State.Unknown},
        new Plane(){Category=Category.Fighter,Name="Su-47",State=State.Unknown},
        new Plane(){Category=Category.Bomber,Name="B-52",State=State.Unknown},
        new Plane(){Category=Category.Fighter,Name="J-10",State=State.Unknown},
    };

    listBoxPlane.ItemsSource = planeList;
}

//Save按鈕Click事件處理器
private void buttonSave_Click(object sender, RoutedEventArgs e)
{
    StringBuilder sb = new StringBuilder();
    foreach (Plane p in listBoxPlane.Items)
    {
        sb.AppendLine(string.Format("Category={0},Name={1},State={2}", p.Category, p.Name, p.State));
    }
    File.WriteAllText(@"D:\PlaneList.txt", sb.ToString());
}

運行程式并單擊CheckBox更改飛機的State,效果如下:

單擊Save按鈕后打開D:\PlaneList.txt,資料如下:

Category=Bomber,Name=B-1,State=Locked
Category=Bomber,Name=B-2,State=Available
Category=Fighter,Name=F-22,State=Available
Category=Fighter,Name=Su-47,State=Unknown
Category=Bomber,Name=B-52,State=Unknown
Category=Fighter,Name=J-10,State=Unknown

MultiBinding(多路Binding)

當UI需要顯示的資訊由不止一個資料來源決定時,需要使用MultiBinding,即多路Binding,MultiBinding與Binding一樣均以BindingBase為基類,凡是能使用Binding物件的場合都能使用MultiBinding,

MultiBinding具有一個名為Bindings的屬性(型別是Collection),通過這個屬性MultiBinding把一組Binding物件聚合起來,處在這個集合中的Binding物件可以擁有自己的資料校驗與轉換機制,它們匯集起來的資料將共同決定傳往MultiBinding目標的資料,示意圖如下所示:

有一個用于新用戶注冊的UI(包含4個TextBox和一個Button),還有如下一些限定:

  • 第一、二個TextBox輸入用戶名,要求內容一致,
  • 第三、四個TextBox輸入用戶E-Mail,要求內容一致,
  • 當TextBox的內容全部符合要求的時候,Button可用,

UI的XAML代碼如下:

<StackPanel Background="LightBlue">
    <TextBox x:Name="textBox1" Height="23" Margin="5"/>
    <TextBox x:Name="textBox2" Height="23" Margin="5,0"/>
    <TextBox x:Name="textBox3" Height="23" Margin="5"/>
    <TextBox x:Name="textBox4" Height="23" Margin="5,0"/>
    <Button x:Name="button1" Content="Sumbit" Width="80" Margin="5"/>
</StackPanel>

設定MultiBinding的代碼,實作Converter,代碼如下:

//準備基礎Binding
Binding b1 = new Binding("Text") { Source = textBox1 };
Binding b2 = new Binding("Text") { Source = textBox2 };
Binding b3 = new Binding("Text") { Source = textBox3 };
Binding b4 = new Binding("Text") { Source = textBox4 };
//準備MultiBinding
MultiBinding mb = new MultiBinding() { Mode = BindingMode.OneWay };
mb.Bindings.Add(b1); //注意:MultiBinding對于Add子Binding的順序是敏感的
mb.Bindings.Add(b2);
mb.Bindings.Add(b3);
mb.Bindings.Add(b4);
mb.Converter = new LogonMultiBindingConverter();
//將Button與MultiBinding物件關聯
button1.SetBinding(Button.IsEnabledProperty, mb);

//Converter
public class LogonMultiBindingConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (!values.Cast<string>().Any(text => string.IsNullOrEmpty(text))
            && values[0].ToString() == values[1].ToString()
            && values[2].ToString() == values[3].ToString())
        {
            return true;
        }
        return false;
    }

    //不會被呼叫
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

需要注意以下幾點:

  • MultiBinding對于添加子級Binding的順序是敏感的,這個順序決定了匯集到Converter里資料的順序,
  • MultiBinding的Converter實作的是IMultiValueConverter介面

程式效果如下:

參考資料

WPF Binding
XPath 語法
XML Path Language (XPath)
RelativeSource
RelativeSourceMode 列舉

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/259904.html

標籤:WPF

上一篇:屬于我的md5sum程式

下一篇:如何用正則表達替換內容

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more