DataBinding快速入門(2)——Binding源與路徑

      Binding源數據對數據類型沒有太多的要求,只要滿足是對象就可以了。要想實現對象具有自動通知Binding的屬性值已經變化的能力,必須讓類實現INotifyPropertyChanged接口並在屬性的set語句中激發PropertyChanged事件。除了這種方式,還有很多作爲數據源的方式,如下:


1 把控件當作源及Binding標記擴展

      大多數情況下Binding源都是來自邏輯層,又是UI元素也需要產生聯動。如把一個TextBox的Text屬性關聯在Slider的Value屬性上:

<Window x:Class="Demo01.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Control as Sourse" Height="110" Width="300">
<StackPanel>
<TextBox x:Name="textBox1" Text="{Binding Path=Value,ElementName=slider1}" BorderBrush="Black"
Margin="5"/>
<Slider Margin="5" x:Name="slider1" Maximum="100" Minimum="0"/>
</StackPanel>
</Window>
       運行效果如下:

注意:在C#代碼中可以訪問XAML中聲明的變量,但XAML代碼無法訪問C#中聲明的變量。

<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" });
// ElementName是獲取或設置要用作綁定源對象的元素的名稱

      Binding類構造器本身就可以接收Path參數,因此可以簡寫如下:

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

注意:C#代碼可以直接訪問控件對象,所以一般不使用Binding的ElementName屬性,而是直接把對象複製給Binding的Source屬性。

      “Text="{BindingPath=Value,ElementName=slider1}"實際就是Binding的簡單標記擴展,其實簡單理解這條語句,就是賦值。但是可以理解爲“爲Text屬性設置Binding爲……”。


2 控制Binding的方向及數據更新

      Binding可以理解爲橋樑,你就是橋樑上總都督。有時只需要把數據展示給用戶,不允許修改,可修改Binding模式爲單向溝通。並且支持數據流通次數。通過屬性值Mode控制數據流向,它的參數值如下:

//描述綁定中數據流的方向。
public enum BindingMode
{
//導致對源屬性或目標屬性的更改可自動更新對方。此綁定類型適用於可編輯窗體或其他完全交互式 UI 方案。

TwoWay = 0,
//當綁定源(源)更改時,更新綁定目標(目標)屬性。如果要綁定的控件爲隱式只讀控件,則適用此綁定類型。
//例如,可以綁定到如股市代號之類的源。或者,可能目標屬性沒有用於進行更改(例如表的數據綁定背景色)的控件接口。
//如果不需要監視目標屬性的更改,則使用
//System.Windows.Data.BindingMode.OneWay 綁定模式可避免 System.Windows.Data.BindingMode.TwoWay
//綁定模式的系統開銷。
OneWay = 1,
//當應用程序啓動或數據上下文更改時,更新綁定目標。此綁定類型適用於以下情況:
//使用當前狀態的快照適合使用的或數據狀態實際爲靜態的數據。
//如果要從源屬性初始化具有某個值的目標屬性,並且事先不知道數據上下文,則也可以使用此綁定類型。實質上,這是
//System.Windows.Data.BindingMode.OneWay 綁定的較簡單的形式,它在不更改源值的情況下可提供更好的性能。
OneTime = 2,
//當目標屬性更改時更新源屬性。
OneWayToSource = 3,
//使用綁定目標的默認 System.Windows.Data.Binding.Mode 值。
//每個依賴項屬性的默認值都不同。一般情況下,用戶可編輯控件屬性(例如文本框和複選框的屬性)默認爲雙向綁定,
//而多數其他屬性默認爲單向綁定。確定依賴項屬性綁定在默認情況下是單向還是雙向的編程方法是:使用
//System.Windows.DependencyProperty.GetMetadata(System.Type) 來獲取屬性的屬性元數據,然後檢查
//System.Windows.FrameworkPropertyMetadata.BindsTwoWayByDefault 屬性的布爾值。
Default = 4,
}
       Default指Binding模式根據目標的實際情況來確定,比如可編輯的(TextBox.Text屬性),Default採用雙向模式;若是隻讀的(TextBlock.Text)則採用單向模式。

       以上程序例子中,當拖動Slider,TextBox就會顯示Slider當前的值。在TextBox裏輸入一個恰當的值,然後按一下Tab鍵,焦點離開TextBox,Slider會跳到相應的值。可以通過UpdateSourceTrigger屬性改變,如下:

//描述綁定源更新的執行時間。
public enum UpdateSourceTrigger
{
//綁定目標屬性的默認 System.Windows.Data.UpdateSourceTrigger 值。多數依賴項屬性的默認值爲
//System.Windows.Data.UpdateSourceTrigger.PropertyChanged,而
//System.Windows.Controls.TextBox.Text 屬性的默認值爲 System.Windows.Data.UpdateSourceTrigger.LostFocus。
Default = 0,
//當綁定目標屬性更改時,立即更新綁定源。
PropertyChanged = 1,
//當綁定目標元素失去焦點時,更新綁定源。
LostFocus = 2,
//僅在調用 System.Windows.Data.BindingExpression.UpdateSource() 方法時更新綁定源。
Explicit = 3,
}

      Binding還有NotifyOnSourceUpdated和NotifyOnTargetUpdated是bool類型,設置爲true,則當數據源或者目標被更新,Binding會激發SourceUpdated和TargetUpdated事件。可以通過偵查這兩個事件來找出那些源數據或UI被更新了。


3 Binding的Path

      Binding源對象可能有很多屬性,通過Path指定需要綁定的屬性。如下創建Path來應對不同的情況:


3.1 通常情況,把Binding關聯在Binding源的屬性上

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

      C#等效代碼如下:

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

      或用Binding構造器簡寫:

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


3.2 Binding支持多級路徑

      如在一個TextBox顯示另一個TextBox的文本長度:

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

      C#等效代碼如下:

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

      集合類型具有索引器功能,而且其實就是帶參屬性,既然是屬性,因此也可以作爲Path來使用。

      如讓TextBox顯示另一個TextBox文本的第四個字符:

<StackPanel>
<TextBox Margin="5" BorderBrush="Black" Name="textBox1" Text="ABCDE" />
<TextBox Margin="5" BorderBrush="Black" Name="textBox2" Text="{Binding Path=Text[3],
ElementName=textBox1,Mode=OneWay}"/>
</StackPanel>

     C#等效代碼如下:

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


3.3 使用一個集合或者DataView作爲Binding源

<Window x:Class="Demo01.Window3"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window3" Height="300" Width="300">
<StackPanel Height="229" Name="stackPanel1" Width="252">
<TextBox Height="23" Name="textBox1" Width="120" Margin="10" />
<TextBox Height="23" Name="textBox2" Width="120" Margin="10" />
<TextBox Height="23" Name="textBox3" Width="120" Margin="10" />
</StackPanel>
</Window>

      後臺代碼如下:

public Window3()
{
InitializeComponent();

List<string> infos = new List<string>() { "Jim", "Darren", "Jacky" };
textBox1.SetBinding(TextBox.TextProperty, new Binding("/") { Source = infos });
textBox2.SetBinding(TextBox.TextProperty, new Binding("/[2]") { Source = infos,
Mode = BindingMode.OneWay });
textBox3.SetBinding(TextBox.TextProperty, new Binding("/Length") { Source = infos,
Mode = BindingMode.OneWay });
}

      如果集合元素下還是一個集合元素,用斜槓選擇下一級。
      再舉一例:
<Window x:Class="Demo01.Window4"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window4" Height="300" Width="300">
<StackPanel Margin="10,10,12,12" Name="stackPanel1">
<TextBox Height="23" Margin="10" Name="textBox1" Width="120" />
<TextBox Height="23" Margin="10" Name="textBox2" Width="120" />
<TextBox Height="23" Margin="10" Name="textBox3" Width="120" />
</StackPanel>
</Window>
      後臺代碼如下:
public partial class Window4 : Window
{
public Window4()
{
InitializeComponent();
List<Contry> infos = new List<Contry>() { new Contry() { Name = "中國",
Provinces= new List<Province>()
{ new Province(){ Name="四川",Citys=new List<City>(){new City(){Name="綿陽市"}}}}}};
this.textBox1.SetBinding(TextBox.TextProperty, new Binding("/Name")
{ Source = infos });
this.textBox2.SetBinding(TextBox.TextProperty, new Binding("/Provinces/Name")
{ Source = infos });
this.textBox3.SetBinding(TextBox.TextProperty, new Binding("/Provinces/Citys/Name")
{ Source = infos });
}
}

class City
{
public string Name { set; get; }
}

class Province
{
public string Name { set; get; }
public List<City> Citys { set; get; }
}

class Contry
{
public string Name { set; get; }
public List<Province> Provinces { get; set; }
}

      運行效果如下:


4 自動尋找數據源

      Binding本身就是數據且不需要Path指明。如string、int等基本類型,它們本身就是數據,沒有屬性,因此可以把Path的值設置爲“.”。在XAML中可以省略“.”,但在C#代碼中不可省略。如下舉例:

<Window x:Class="Demo01.Window5"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Str="clr-namespace:System;assembly=mscorlib"
Title="Window5" Height="110" Width="300">
<StackPanel>
<StackPanel.Resources>
<Str:String x:Key="myString">
菩提本無樹,何處染塵埃。
</Str:String>

</StackPanel.Resources>
<TextBlock x:Name="textBlock1" TextWrapping="Wrap" Text="{Binding Path=.,Source=
{StaticResource ResourceKey=myString}}" FontSize="30"/>
</StackPanel>
</Window>
       運行效果如下:

      也可以簡寫:

<TextBlock x:Name="textBlock1" TextWrapping="Wrap" Text=
"{Binding .,Source={StaticResource ResourceKey=myString}}" FontSize="30"/>
      或:
<TextBlock x:Name="textBlock1" TextWrapping="Wrap" Text=
"{Binding Source={StaticResource ResourceKey=myString}}" FontSize="30"/>

      用C#代碼替代:

string myString = "菩薩本無樹,明鏡亦非臺。本來無一物,何處惹塵埃。";
this.textBlock1.SetBinding(TextBlock.TextProperty, new Binding(".") { Source = myString });


5 爲Binding指定源的幾種方法

      Binding的源其實只要一個對象包含數據並通過屬性把數據暴露出來,它就能當作Binding的源來使用。常見的方法有:

  • 普通CLR對象指定爲Source,包括.NETFramework自帶類型對象和用戶自定義對象。類實現INotifyPropertyChanged接口,可以通過在屬性的set語句裏激發PropertyChanged事件來通知Bingding數據更新。
  • 普通CLR集合類型對象指定爲Source,包括數組、List、ObservableCollection等集合類型。通常把集合作爲ItemsControl派生類的數據來源使用
  • ADO.NET數據對象指定爲Source,包括DataTable和DataView等。
  • 使用XmlDataProvider把XML數據指定爲Source。如一些WPF級聯式控件(TreeView和Menu),可以把樹狀結構的XML與之關聯。
  • 把依賴對象(Dependency Object)指定爲數據源。既可以做數據源又可以做目標,因此可以形成Binding鏈。
  • 容器的DataContext指定爲數據源(Binding默認)。如有時不明確源但明確屬性,即只設置Path不設置Source。Binding會沿着控件樹一層一層的找屬性,並把DataContext當作自己的Source。
  • 通過ElementName指定Source。但在C#代碼中可以直接把對象指定Source。
  • 通過Binding的RelativeSource屬性相對地指定Source。控件只關心自己或自己的容器及內部元素時用該方法。
  • 用ObjectDataProvider對象指定爲Source。當數據源的數據不是通過屬性而是通過方法的時候使用。
  • 把LiNQ檢索得到的數據對象作爲數據源。


5.1 使用DataContext作爲Binding的源(無Source)

      DataContext屬於FrameworkElement類,這個類是WPF控件的基類,因此基本所有的WPF控件都具備這個屬性。WPF的UI佈局是樹形結構。因此Binding會沿着樹節點一個一個的找,找到Path指定的屬性了,才把該對象當成Source。否則會一路找下去,如果到根節點還沒找到,則不會賦值給Source。

       如下例子所示,先創建一個StudentInfo類:

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

      UI端代碼:

<Window x:Class="Demo01.Window6"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Stu="clr-namespace:Demo01"
Title="Window6" Height="345" Width="464">
<StackPanel Background="AliceBlue">
<StackPanel.DataContext>
<Stu:StudentInfo Id="1" Name="Darren" Age="10"></Stu:StudentInfo>
</StackPanel.DataContext>
<Grid>
<StackPanel Height="283" HorizontalAlignment="Left" Margin="12,12,0,0" Name="stackPanel1"
VerticalAlignment="Top" Width="418">
<TextBox Height="23" Name="textBox1" Width="120" Margin="15" Text="{Binding Path=Id}"/>
<TextBox Height="23" Name="textBox2" Width="120" Margin="15" Text="{Binding Path=Name}"/>
<TextBox Height="23" Name="textBox3" Width="120" Margin="15" Text="{Binding Path=Age}"/>
</StackPanel>
</Grid>
</StackPanel>
</Window>

      UI佈局如下所示:

      使用xmlns:Stu=使用“clr-namespace:Demo01”就可以在XAML中使用Student類了。對DataContext進行賦值——它是一個Student對象。
<StackPanel.DataContext>
<Stu:StudentInfo Id="1" Name="Darren" Age="10"></Stu:StudentInfo>
</StackPanel.DataContext>

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

<TextBox Height="23" Name="textBox1" Width="120" Margin="15" Text="{Binding Id}"/>
<TextBox Height="23" Name="textBox2" Width="120" Margin="15" Text="{Binding Name}"/>
<TextBox Height="23" Name="textBox3" Width="120" Margin="15" Text="{Binding Age}"/>

      TextBox的Binding會自動向UI元素樹的上層去尋找Path匹配的DataContext對象,效果如下:


      前面部分已經指出Binding如果關聯基本數據類型時,就不需要指定Path了。同理,當某個DataContext是一個簡單類型對象的時候,就可以既不設置Path也不設置Source了。

<Window x:Class="Demo01.Window7"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Str="clr-namespace:System;assembly=mscorlib"
Title="Window7" Height="300" Width="300">
<Grid>
<Grid.DataContext>
<Str:String>Hello DataContext</Str:String>
</Grid.DataContext>
<StackPanel>
<TextBlock Height="23" HorizontalAlignment="Left" Margin="15" Name="textBlock1" Text="{Binding}"
VerticalAlignment="Top" />
<TextBlock Height="23" HorizontalAlignment="Left" Margin="15" Name="textBlock2" Text="{Binding}"
VerticalAlignment="Top" />
<TextBlock Height="23" HorizontalAlignment="Left" Margin="15" Name="textBlock3" Text="{Binding}"
VerticalAlignment="Top" />
</StackPanel>
</Grid>
</Window>
       運行效果如下:

      Binding爲什麼會自動沿着UI樹查找DataContext呢?其實並不是它智能,而是DataContext是一個“依賴屬性”。通過字面意思也很好理解,當你不給依賴屬性顯示賦值的時候,它就會把自己容器的屬性借過來,因此它會一層一層的尋找。如下面的例子所示:

<Window x:Class="Demo01.Window8"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window8" Height="120" Width="240">
<Grid DataContext="6">
<Grid>
<Grid>
<Grid>
<Button x:Name="btn" Content="ok" Click="btn_Click"/>
</Grid>
</Grid>
</Grid>
</Grid>
</Window>

      Button的Click事件如下:

private void btn_Click(object sender,RoutedEventArgs e)
{
MessageBox.Show(btn.DataContext.ToString());
}

      點擊Button運行效果如下:

      由此可見,DataContext是非常靈活的。當UI上多個控件都使用Binding關心同一個對象時,可以用DataContext代替;當作爲Source的對象不能被直接訪問,如B窗體想把A窗體內的控件作爲Source,但是A窗體的控件爲private訪問級別。可以把控件作爲窗體A的DataContext。可以理解爲DataContext就是一座探照塔,只要數據上去,他就問別的元素放出照明光線,別的元素就都能看見了。DataContext就是一個依賴屬性。用Binding可以直接把它關聯到一個數據源上。


5.2 集合對象作爲列表控件的ItemsSource

      WPF列表式控件繼承ItemsConrol類,因此也擁有ItemsSouece屬性。ItemsSouece可接受IEnumerable接口派生類賦值(所以迭代遍歷集合,包括數組、List等)。每個ItemsControl的派生類具有自己對應的條目容器。例如ListBox對應ListBoxItem、ComboBox對應ConboBoxItem。如何Binding呢?只要爲ItemsControl設置ItemsSource屬性值就可以了。如下所示:

<Window x:Class="Demo01.Window9"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Binding Source" Height="240" Width="300">
<StackPanel x:Name="stackPanel" Background="LightBlue">
<TextBlock Margin="5" FontWeight="Bold" x:Name="textBlock1" Text="學員編號:" />
<TextBox Margin="5" x:Name="txtStudentId"/>
<TextBlock Margin="5" FontWeight="Bold" x:Name="textBlock2" Text="學員列表:" />
<ListBox x:Name="lbStudent" Height="110" Margin="5"/>
</StackPanel>
</Window>

      後臺代碼實現把List<StudentInfo>的實例作爲ListBox的ItemSource,ListBox顯示StudentInfo的Name屬性並使用TextBox顯示當前選中條目的Id。後臺代碼如下:

namespace Demo01
{
public partial class Window9 : Window
{
public Window9()
{
InitializeComponent();

List<StudentInfo> listStu = new List<StudentInfo>() {
new StudentInfo(){ Id=1, Age=11, Name="Tom"},
new StudentInfo(){ Id=2, Age=12, Name="Darren"},
new StudentInfo(){ Id=3, Age=13, Name="Jacky"},
new StudentInfo(){ Id=4, Age=14, Name="Andy"}
};

// 爲ListBox設置Binding
this.lbStudent.ItemsSource = listStu;
this.lbStudent.DisplayMemberPath = "Name";

// 爲TextBox設置Binding
Binding bind = new Binding("SelectedItem.Id") { Source = this.lbStudent };
this.txtStudentId.SetBinding(TextBox.TextProperty, bind);
}
}
}

      DisplayMemberPath就相當於Binding的Path。ListBox在獲得ItemsSource的時候就會創建等量的ListBoxItem並以DisPlayMemberPath屬性值爲Path創建Binding,Binding的目標就是ListBoxItem的內容插件。運行效果如下:

      ItemsControl創建Binding的過程是在DisplayMemberTemplateSelector類的SelectTemplate方法裏完成的。格式如下:

public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
//......
}

      它的返回值很重要,它基本上是數據的外殼。當沒有爲ItemsControl顯式地指定DataTemplate時SelectTemplate方法就會爲我們創建一個默認DataTemplate。SelectTemplate與創建Binding相關代碼如下:

FrameworkElementFactory text=ContentPresenter Create TextBlockFactory();
Binding binding=new Binding();
Binding.Path=new Property Path( _displayMemberPath);
binding.StringFormat= _stringFormat;
text.SetBinding(TextBlock.TextProperty, binding);
      通過上述代碼,Binding設置了Path而沒有指定Source,並關聯到TextBlock控件上。所以Binding是通過UI樹尋找_displayMemberPath指定屬性的DataContext。
      也可以顯式設置DataTemplate。把"this.lbStudent.DisplayMemberPath = "Name";"刪除。在XAML中,添加ListBox的ItemTemplate屬性(派生於ItemsControl類),它就是DataTemplate類型。如:
<Window x:Class="Demo01.Window10"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Binding Source" Height="240" Width="300">
<StackPanel x:Name="stackPanel" Background="LightBlue">
<TextBlock Margin="5" FontWeight="Bold" x:Name="textBlock1" Text="學員編號:" />
<TextBox Margin="5" x:Name="txtStudentId"/>
<TextBlock Margin="5" FontWeight="Bold" x:Name="textBlock2" Text="學員列表:" />
<ListBox x:Name="lbStudent" Height="110" Margin="5">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Name="stackPanel2" 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>
</Window>

      運行結果如下:

注意:推薦ObservableCollection<T>代替List<T>,因爲ObservableCollection<T>類實現了INotifyCollectionChanged和INotifyPropertyChanged接口,集合數據變化立刻通知列表控件並顯式出來。


5.3 使用ADO.NET對象作爲Binding的源

      經常會使用ADO.NET類對數據庫進行操作。比如把數據庫中的數據讀取到DataTable中,同時顯示在UI列表控件。但目前通常都是用LINQ等手段把DataTable裏的數據轉換成用戶自定義類型集合。但Binding支持綁定DataTable。

      UI代碼如下:

<Window x:Class="Demo01.Window11"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DataTable Source" Height="206" Width="250">
<StackPanel Background="LightBlue">
<ListBox Height="130" Margin="5" x:Name="listBoxStudents"/>
<Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>
</Window>

      後臺代碼如下:

public partial class Window11 : Window
{
public Window11()
{
InitializeComponent();

}

private DataTable CreateDataTable()
{
DataTable dt = new DataTable("newtable");

DataColumn[] columns = new DataColumn[] { new DataColumn("Id"), new DataColumn("Name"),
new DataColumn("Age"), new DataColumn("Sex") };
dt.Columns.AddRange(columns);
return dt;
}

private void Button_Click(object sender, RoutedEventArgs e)
{
DataTable dtInfo = CreateDataTable();
for (int i = 0; i < 5; i++)
{
DataRow dr = dtInfo.NewRow();
dr[0] = i;
dr[1] = "猴王" + i;
dr[2] = i + 10;
dr[3] = "男";
dtInfo.Rows.Add(dr);
}

this.listBoxStudents.DisplayMemberPath = "Name";
this.listBoxStudents.ItemsSource = dtInfo.DefaultView;
}
}
      點擊按鈕,運行效果如下:

      後臺代碼中DataTable的DefaultView屬性是一個DataView類型的對象,DataView類實現了IEnumerable接口,所以可以被賦值給ListBox.ItemsSource屬性。

      通常情況下會選擇ListView控件來顯示DataTable。如下:

<Window x:Class="Demo01.Window12"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DataTable Source" Height="206" Width="250">
<StackPanel Background="LightBlue">
<ListView x:Name="listViewStudents" Height="130" Margin="5">
<ListView.View>
<GridView>
<GridViewColumn Header="Id" DisplayMemberBinding="{Binding Id}" Width="60">
</GridViewColumn>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" Width="80">
</GridViewColumn>
<GridViewColumn Header="Age" DisplayMemberBinding="{Binding Age}" Width="60">
</GridViewColumn>
<GridViewColumn Header="Sex" DisplayMemberBinding="{Binding Sex}" Width="60">
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>
</Window>

注意:1、初學者經常把ListView和GridView認爲是同一級別的控件,實際是錯誤的!ListView繼承ListBox而GridView繼承ViewBase。ListView的View屬性是一個ViewBase類型,因此GridView可以作爲ListView的View類使用,而不能當做獨立的控件使用。但ListView的View不代表只能用GridView。

2、GridView的內容屬性是Columns,它是GridViewColumnCollection類型。因爲XAML語法支持簡寫,所以省略了<GridView.Columns>...</GridView.Columns>這層標籤,而是直接在<GridView>下定義了三個GridViewColumn對象,用DisPlayMemberBinding(類型爲BindingBase)屬性指定Binding關聯的數據。它與ListBox不同,ListBox使用的是DisplayMemberPath屬性。如果想用複雜結構表示,則可謂GridViewColumn設置HeaderTemplate和CellTemplate屬性,它們的類型是DataTemplate。

      後臺代碼如下:

public partial class Window12 : Window
{
public Window12()
{
InitializeComponent();
}

private DataTable CreateDataTable()
{
DataTable dt = new DataTable("newtable");

DataColumn[] columns = new DataColumn[] { new DataColumn("Id"), new DataColumn("Name"),
new DataColumn("Age"), new DataColumn("Sex") };
dt.Columns.AddRange(columns);
return dt;
}

private void Button_Click(object sender, RoutedEventArgs e)
{
DataTable dtInfo = CreateDataTable();
for (int i = 0; i < 5; i++)
{
DataRow dr = dtInfo.NewRow();
dr[0] = i;
dr[1] = "猴王" + i;
dr[2] = i + 10;
dr[3] = "男";
dtInfo.Rows.Add(dr);
}

this.listViewStudents.ItemsSource = dtInfo.DefaultView;
}
}

      點擊按鈕運行效果如下:


      如果把程序改寫,拿DataTable直接作爲ItemsSource會報錯。

this.listViewStudents.ItemsSource = dtInfo;     

     
   不過,將DataTable對象放在一個對象的DataContext屬性裏,並把ItemsSource與一個既沒有指定Source又沒有指定Path的Binding關聯起來,Binding會自動找到DefaultView。這裏不做解釋了,前面已經提到DataContext的尋找方式及原因。

this.listViewStudents.DataContext = dtInfo;
this.listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());


5.4 XML作爲源

      .NET Framework基本上用兩種處理XML的方式:

  •  DOM(Document Object Model 文檔對象模型)類庫:包括XMLDocument、XmlElement、XmlNode、XmlAttribute等類。它的優點是中規中矩喝功能強大,缺點是使用時有很多XML的傳統和複雜。
  • LINQ(Language Integrated Qurey 語言集成查詢)類庫:包括XDocument、XElement、XNode、XAttribute等類。它主要能使用LINQ查詢和操作,比DOM的方式簡單很多。

      先總結DOM操作XML的類庫。爲什麼XML在現在開發中比較常用呢?因爲絕大部分的傳輸協議都是基於SOAP(Simple Object Access Protocol簡單對象訪問協議)相關協議,SOAP就是通過將對象序列化XXML進行傳輸的。XML是樹形結構,很方便表示線性集合(如Array、List等)和樹形結構的數據。

注意:使用XML作爲Binding數據源時,Path是XPath來指定屬性。

      下面是一個簡單XML文本表示學生信息:

<?xml version="1.0" encoding="utf-8" ?>
<StudentList>
<Student id="1">
<Name>Andy</Name>
</Student>
<Student id="2">
<Name>Jacky</Name>
</Student>
<Student id="3">
<Name>Darren</Name>
</Student>
<Student id="4">
<Name>DK</Name>
</Student>
<Student id="5">
<Name>Jim</Name>
</Student>
</StudentList>

      UI部分代碼:

<Window x:Class="Demo01.Window13"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="XML Source" Height="205" Width="240">
<StackPanel Background="LightBlue">
<ListView x:Name="listViewStudents" Height="130" Margin="5">
<ListView.View>
<GridView>
<GridViewColumn Header="Id" DisplayMemberBinding="{Binding XPath=@id}" Width="80">
</GridViewColumn>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding XPath=Name}" Width="120">
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>
</Window>

      後臺的Button事件:

private void Button_Click(object sender, RoutedEventArgs e)
{
XmlDocument doc = new XmlDocument();
doc.Load(@"..\..\StudentData.xml");

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

// 選擇要暴露的數據
xdp.XPath = @"StudentList/Student";
this.listViewStudents.DataContext = xdp;
this.listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
}
      點擊Button運行效果如下:

      XmlDataProvider還有一個Source的屬性,可以指定XML文檔的位置,如:

private void Button_Click(object sender, RoutedEventArgs e)
{
XmlDataProvider dp = new XmlDataProvider();
dp.Source = new Uri(@"..\..\StudentData.xml", UriKind.Relative);

dp.XPath = @"StudentList/Student";
this.listViewStudents.DataContext = dp;
this.listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
}

     上述XAML代碼中你會發現關鍵代碼“<GridViewColumn Header="Id"DisplayMemberBinding="{Binding XPath=@id}" Width="80">”和“<GridViewColumn Header="Name"DisplayMemberBinding="{Binding XPath=Name}”這裏‘@’符號代表的XML元素的Attribute,不加‘@’符合的字符串表示的是子級元素。

      XML可以方便的表示樹形結構,下面例子是使用TreeView控件來顯示文件系統目錄,下面直接把代碼寫在XAML中。代碼中用到了HierarchicalDataTemplate類,這個類有ItemsSource屬性,因此這種類型是可以擁有子集集合的。

<Window x:Class="Demo01.Window14"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Xml Source" Height="210" Width="260">
<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>
<Folder Name="Winform">

</Folder>
<Folder Name="ASP.NET">

</Folder>
</Folder>
</Folder>
</Folder>
<Folder Name="Tools">
<Folder Name="Development"/>
<Folder Name="Designment"/>
<Folder Name="Players"/>
</Folder>
</FileSystem>
</x:XData>
</XmlDataProvider>
</Window.Resources>
<Grid>
<TreeView Height="283" HorizontalAlignment="Left" Name="treeView1" VerticalAlignment="Top"
Width="511" ItemsSource="{Binding Source={StaticResource ResourceKey=xdp}}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding XPath=Folder}">
<TextBlock Height="23" HorizontalAlignment="Left" Name="textBlock1"
Text="{Binding XPath=@Name}" VerticalAlignment="Top" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
注意:XmlDataProvider直接寫在XAML裏,XML數據要放在<x:XData>...</x:XData>裏。

      代碼中涉及StaticResouce和HierarchicalDataTemplate,暫時不做詳細說明,在這裏只需簡單理解即可。

      運行效果如下:


5.5 使用LINQ檢索結果作爲Binding數據源

      .NET Framework提供LINQ(Language-Integrated Query 語言集成查詢),可以方便操作集合對象、DataTable對象和XML對象。並且LINQ查詢結果就是一個IRnumerable<T>類型對象,IRnumerable<T>繼承於IRnumerable,所以它可以作爲列表控件的ItemsSource來使用。

      之前已經寫過StudentInfo的類了,現在根據這個類設計一個單擊Button顯示一個Students集合類型對象。

<Window x:Class="Demo01.Window15"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="LINQ Source" Height="220" Width="280">
<StackPanel Background="LightBlue">
<ListView Height="143" Margin="5" Name="listViewStudents">
<ListView.View>
<GridView>
<GridViewColumn Header="Id" DisplayMemberBinding="{Binding Id}" Width="60"/>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" Width="100"/>
<GridViewColumn Header="Age" DisplayMemberBinding="{Binding Age}" Width="80"/>
</GridView>
</ListView.View>
</ListView>
<Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>
</Window>

      下面是button事件,從StudentInfo列表中檢索出所有名字以'T'開頭的學生。

private void Button_Click(object sender, RoutedEventArgs e)
{
List<StudentInfo> infos = new List<StudentInfo>()
{
new StudentInfo(){Id=1, Age=29, Name="Tim"},
new StudentInfo(){Id=1, Age=28, Name="Tom"},
new StudentInfo(){Id=1, Age=27, Name="Kyle"},
new StudentInfo(){Id=1, Age=26, Name="Tony"},
new StudentInfo(){Id=1, Age=25, Name="Vina"},
new StudentInfo(){Id=1, Age=24, Name="Mike"}
};
this.listViewStudents.ItemsSource = from stu in infos where stu.Name.StartsWith("T") select stu;
}

      運行效果如下:

     

       如果是DataTable對象,則代碼如下:

private void Button_Click1(object sender, RoutedEventArgs e)
{
DataTable dtInfo = CreateDataTable();
for (int i = 0; i < 5; i++)
{
DataRow dr = dtInfo.NewRow();
dr[0] = i;
if (0 == i % 2)
{
dr[1] = "T猴王" + i;
}
else
{
dr[1] = "猴王" + i;
}
dr[2] = i + 10;
dtInfo.Rows.Add(dr);
}

this.listViewStudents.ItemsSource = from row in dtInfo.Rows.Cast<DataRow>()
where Convert.ToString(row["Name"]).StartsWith("T")
select new StudentInfo()
{
Id = Convert.ToInt32(row["Id"]),
Name = Convert.ToString(row["Name"]),
Age = Convert.ToInt32(row["Age"])
};
}

private DataTable CreateDataTable()
{
DataTable dt = new DataTable("newtable");

DataColumn[] columns = new DataColumn[] { new DataColumn("Id"), new DataColumn("Name"),
new DataColumn("Age") };
dt.Columns.AddRange(columns);
return dt;
}
}

       運行效果如下:


      如果是XML文件,代碼如下:
<?xml version="1.0" encoding="utf-8" ?>
<StudentList>
<Class>
<Student Id="0" Age="29" Name="Tim" />
<Student Id="1" Age="28" Name="Tom" />
<Student Id="2" Age="27" Name="Mess" />
</Class>
<Class>
<Student Id="3" Age="26" Name="Tony" />
<Student Id="4" Age="25" Name="Vina" />
<Student Id="5" Age="24" Name="Emily" />
</Class>
</StudentList>
     後臺代碼如下:
private void Button_Click2(object sender, RoutedEventArgs e)
{
XDocument xd = XDocument.Load(@"..\..\testDate.xml");

this.listViewStudents.ItemsSource = from element in xd.Descendants("Student")
where element.Attribute("Name").Value.StartsWith("T")
select new StudentInfo()
{
Name = element.Attribute("Name").Value,
Id = Convert.ToInt32(element.Attribute("Id").Value),
Age = Convert.ToInt32(element.Attribute("Age").Value)
};
}
      運行效果如下:


5.6 使用ObjectDataProvider對象作爲Binding

      使用Binding很多時候很難保證一個類所有數據都用屬性暴露出來,如我們需要的數據是方法的而返回值。重新設計底層類的風險會很高,比如是黑盒引用類庫是無法編輯源碼的,現在就可以用ObjectDataProvider作爲Binding源了。其實就是把對象作爲數據源的意思,如還有XmlDataProvider,就是把XML數據作爲源。這兩個類的父類都是DataSourceProvider抽象類。

      新建一個Caculate類,目前只有加法運算:

public class Caculate
{
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 "Iput Error";
}

//其它方法省略
}

      先用一個很簡單的小例子使用ObjectDataProvider類,界面上只有一個button,前臺代碼如下:

<Window x:Class="Demo01.Window16"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window16" Height="300" Width="300">
<Grid>
<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="10,10,0,0" Name="button1"
VerticalAlignment="Top" Width="75" Click="button1_Click" />
</Grid>
</Window>
      button的Click事件處理器如下:
private void button1_Click(object sender, RoutedEventArgs e)
{
ObjectDataProvider odp = new ObjectDataProvider();
odp.ObjectInstance = new Caculate();
odp.MethodName = "Add";
odp.MethodParameters.Add("100");
odp.MethodParameters.Add("200");
MessageBox.Show(odp.Data.ToString());
}
      點擊button,運行效果如下:

       通過上面舉的例子,相信菜鳥們也能理解ObjectDataProvider的使用,其實也就通過它的幾個屬性進行操作,如包裝的對象(ObjectInstance)、傳入參數(MethodParameters)、方法名稱(MethodName)、返回結果(Data)進行組合操作。詳細操作,請看下面的例子:

<Window x:Class="Demo01.Window17"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ObjectDataProvider Source" Height="300" Width="300">
<StackPanel Background="LightBlue">
<TextBox Name="textBoxArg1" Margin="5"/>
<TextBox Name="textBoxArg2" Margin="5"/>
<TextBox Name="textBoxResult" Margin="5"/>
</StackPanel>
</Window>

      這個例子要實現的邏輯是在前兩個TextBox輸入數字後,第三個TextBox能實時顯示數字的和。後臺代碼如下:

public partial class Window17 : Window
{
public Window17()
{
InitializeComponent();
SetBinding();
}

private void SetBinding()
{
ObjectDataProvider objpro = new ObjectDataProvider();
objpro.ObjectInstance = new Caculate();

objpro.MethodName = "Add";
objpro.MethodParameters.Add("0");
objpro.MethodParameters.Add("0");
Binding bindingToArg1 = new Binding("MethodParameters[0]") { Source = objpro,
BindsDirectlyToSource = true, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged };
Binding bindingToArg2 = new Binding("MethodParameters[1]") { Source = objpro,
BindsDirectlyToSource = true, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged };
Binding bindToResult = new Binding(".") { Source = objpro };
this.textBoxArg1.SetBinding(TextBox.TextProperty, bindingToArg1);
this.textBoxArg2.SetBinding(TextBox.TextProperty, bindingToArg2);
this.textBoxResult.SetBinding(TextBox.TextProperty, bindToResult);
}
}

      運行效果如下:

      ObjectDataProvider類的作用是用來包裝一個以方法暴露數據的對象,將Calculate對象賦值給ObjectInstance屬性。還有另外一種辦法創建被包裝的對象,告訴ObjectDataProvider將被包裝對象的類型{odp.ObjectType = typeof(YourClass)}和希望調用的構造器。如果指定的MethodName是一個重載函數,但是通過MethodParameters就可以指定參數了,該例子就是調用兩個String類型參數的Add方法。最後就是創建Binding了,參數中索引器作爲Path,第一個元素指定數據源是ObjectDataProvider對象,第二個元素BindsDirectlyToSource = true,意思是Binding對象只負責從UI元素收集到數據寫入Source(ObjectDataProvider對象)而不是被ObjectDataProvider包裝的Calculate對象。第三個元素UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,意思是一有更新立刻將值傳回Source。bindToResult實例,創建Binding用“.”是指定自己本身爲Path。

      這個例子,三個TextBox都以ObjectDataProvider對象作爲數據源,只是前兩個TextBox在Binding的數據流向上做了限制。因爲ObjectDataProvider的MethodParameters不是依賴屬性,不能作爲Binding的目標。數據驅動UI的理念要求儘可能使用數據對象作爲Binding的Source而把UI元素當作目標。

5.7 使用Binding的RelativeSource

      我們經常通過Source或ElementName指定Source,有時目標的對象在UI佈局上有相對關係,比如控件關聯自己某級容器的數據。RelativeSource屬性的數據類型爲RelativeSource類,通過它的屬性控制搜索相對數據源。下面多層佈局控件內放置一個TextBox,如下:

<Window x:Class="Demo01.Window18"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="RelativeSource"Height="210" Width="210">
<GridBackground="Red" Margin="10" x:Name="gd1">
<DockPanel x:Name="dp1" Margin="10" Background="Orange">
<Grid Background="Yellow" Margin="10" x:Name="gd2">
<DockPanel Name="dp2"Margin="10" Background="LawnGreen">
<TextBox Name="textBox1" Margin="10" FontSize="24"/>
</DockPanel>
</Grid>
</DockPanel>
</Grid>
</Window>

      運行效果如下:


      把Text屬性關聯到外層容器的Name屬性上。如下:

public Window18()
{
InitializeComponent();

RelativeSource rs = new RelativeSource(RelativeSourceMode.FindAncestor);
rs.AncestorLevel = 1;
rs.AncestorType = typeof(Grid);
Binding bind = new Binding("Name") { RelativeSource = rs };
this.textBox1.SetBinding(TextBox.TextProperty, bind);
}

      XAML等效代碼如下:

<TextBox Name="textBox1" Margin="10" FontSize="24" Text="{Binding RelativeSource=
{RelativeSource FindAncestor,AncestorType={x:Type Grid}, AncestorLevel=1}, Path=Name}"/>

       AncestorLevel屬性指的是以Binding目標控件爲起點的層級偏移量。如dp2的偏移量是1,gd2的偏移量是2.AncestorType屬性指定Binding尋找對應類型的Source,不是這個類型就會被跳過。因此上述代碼會跳過第一層的DockPanel,而是找到第一個Grid類型對象後當作自己的源。

       運行效果如下:


       如果把代碼改成:

public Window19()
{
InitializeComponent();

RelativeSource rs = new RelativeSource(RelativeSourceMode.FindAncestor);
rs.AncestorLevel = 2;
rs.AncestorType = typeof(DockPanel);
Binding bind = new Binding("Name") { RelativeSource = rs };
this.textBox1.SetBinding(TextBox.TextProperty, bind);
}

        XAML替換代碼如下:

<TextBox Name="textBox1" Margin="10" FontSize="24" Text="{Binding RelativeSource=
{RelativeSource FindAncestor, AncestorType={x:Type DockPanel}, AncestorLevel=2}, Path=Name}"/>

       運行效果如下:


       如果TextBox需要關聯自身的Name屬性,則前臺代碼如下:

<Window x:Class="Demo01.Window20"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="RelativeSource" Height="210" Width="210">
<Grid Background="Red" Margin="10" x:Name="gd1">
<DockPanel x:Name="dp1" Margin="10" Background="Orange">
<Grid Background="Yellow" Margin="10" x:Name="gd2">
<DockPanel Name="dp2" Margin="10" Background="LawnGreen">
<TextBox Name="textBox1" Margin="10" FontSize="24"/>
</DockPanel>
</Grid>
</DockPanel>
</Grid>
</Window>

        後臺代碼如下:

public Window20()
{
InitializeComponent();
RelativeSource rs = new RelativeSource();
rs.Mode = RelativeSourceMode.Self;
Binding bind = new Binding("Name") { RelativeSource = rs };
this.textBox1.SetBinding(TextBox.TextProperty, bind);
}

        運行效果如下:


       RelativeSource類的Modw屬性類型是RelativeSourceMode枚舉類型,包括:PreviousData、TemplatedParent、Self和FindAncestor。RelativeSource類還有3個靜態屬性PreviousData、TemplatedParent、Self,類型均是RelativeSource類。實際上是創建一個RelativeSource實例,把實例的Mode屬性設置相應的值,然後返回。準備這三個實例是方便XAML直接獲取RelativeSource實例。在DataTemplate中會經常用到這三個靜態屬性,下面是源碼:

public static RelativeSource PreviousData
{
get
{
if(s_previousData==null)
{
s_previousData = new RelativeSource(RelativeSourceMode.PreviousData);
}
return s_previousData;
}
}

public static RelativeSource TemplatedParent
{
get
{
if (s_templatedParent == null)
{
s_templatedParent = new RelativeSource(RelativeSourceMode.TemplatedParent);
}
return s_templatedParent;
}
}

public static RelativeSource Self
{
get
{
if (s_self == null)
{
s_self = new RelativeSource(RelativeSourceMode.Self);
}
return s_self;
}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章