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控制数据流向,它的参数值如下:

扫描二维码关注公众号,回复: 11122786 查看本文章

//描述绑定中数据流的方向。
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;
}
}
发布了34 篇原创文章 · 获赞 12 · 访问量 2537

猜你喜欢

转载自blog.csdn.net/u013946061/article/details/80741353