数据绑定
NoesisGUI提供了一种简单而强大的方法来自动更新业务模型和用户界面之间的数据。这种机制称为数据绑定。它们是数据绑定的关键,是一个绑定对象,该对象将两个属性“粘合”在一起并保持它们之间的通信通道畅通。您可以设置一次绑定,然后让它在应用程序的剩余生命周期中完成所有同步工作。
在本教程中,我们将说明您可能希望使用数据绑定的不同方式。
在XAML中使用绑定
DataBindingTutorialImg1.jpg要在XAML中使用绑定,可以直接将target属性设置为Binding实例,然后使用标准标记扩展语法设置其属性。下面的示例显示了TextBox的文本和Label之间的简单绑定,该绑定反映了键入的值:
<StackPanel
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
VerticalAlignment="Center">
<TextBox x:Name="textbox" />
<Label Content="{Binding Text, ElementName=textbox}" />
</StackPanel>
注意
数据绑定的来源可以是常规属性或DependencyProperty。绑定的目标属性必须是DependencyProperty。
数据语境
同一UI中的许多元素绑定同一源对象是很常见的。因此,noesisGUI支持指定隐式数据源,而不是使用 Source,RelativeSource或ElementName显式标记每个Binding。此隐式数据源也称为数据上下文。
要将源对象指定为数据上下文,只需找到一个公共父元素并将其DataContext属性设置为该源对象即可。在没有显式源对象的情况下遇到 Binding时,noesisGUI会遍历逻辑树,直到找到非null的DataContext为止。这是一个设置数据上下文的示例,该示例将用于以后的所有示例:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
<DataModel x:Name="dataModel" />
</UserControl.Resources>
<StackPanel DataContext="{StaticResource dataModel}" />
</UserControl>
注意
在实际项目中,更常见的是通过代码设置数据上下文
绑定到普通属性
NoesisGUI支持将任何对象的任何常规属性用作数据绑定源。例如,以下XAML绑定了几个属性,这些属性属于在当前数据上下文中找到的实例:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
<DataModel1 x:Name="dataModel">
<DataModel1.Person>
<Person Weight="90">
<Person.Name>
<Name First="John" Last="Doe" />
</Person.Name>
</Person>
</DataModel1.Person>
</DataModel1>
</UserControl.Resources>
<StackPanel DataContext="{StaticResource dataModel}" HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="{Binding Person.Name.First}" />
<TextBlock Text="{Binding Person.Name.Last}" />
<TextBlock Text="{Binding Person.Weight}" />
</StackPanel>
</UserControl>
但是,使用平原属性作为数据绑定源存在很大的警告。由于此类属性没有自动检测更改通知的对象,因此在不做任何额外工作的情况下,由于源属性值发生更改,因此目标无法保持最新。为了使目标属性和源属性保持同步,源对象必须实现INotifyPropertyChanged接口。
C ++实现
每当在C ++中实现数据模型时,都必须注意一些重要的细节:
- 基类NotifyPropertyChangedBase已经为我们实现了INotifyPropertyChanged接口
- 使用带有getter和setter函数的反射宏宏公开属性
- 在每个设置器中,检查是否必须触发属性更改事件
C ++
class Name: public NotifyPropertyChangedBase
{
public:
const char* GetFirst() const
{
return _first.c_str();
}
void SetFirst(const char* first)
{
if (_first != first)
{
_first = first;
OnPropertyChanged("First");
}
}
const char* GetLast() const
{
return _last.c_str();
}
void SetLast(const char* last)
{
if (_last != last)
{
_last = last;
OnPropertyChanged("Last");
}
}
private:
NsString _first;
NsString _last;
NS_IMPLEMENT_INLINE_REFLECTION(Name, NotifyPropertyChangedBase)
{
NsMeta<TypeId>("Name");
NsProp("First", &Name::GetFirst, &Name::SetFirst);
NsProp("Last", &Name::GetLast, &Name::SetLast);
}
};
class Person: public NotifyPropertyChangedBase
{
public:
Name* GetName() const
{
return _name.GetPtr();
}
void SetName(Name* name)
{
if (_name != name)
{
_name.Reset(name);
OnPropertyChanged("Name");
}
}
float GetWeight() const
{
return _weight;
}
void SetWeight(const float weight)
{
if (_weight != weight)
{
_weight = weight;
OnPropertyChanged("Weight");
}
}
private:
Ptr<Name> _name;
float _weight;
NS_IMPLEMENT_INLINE_REFLECTION(Person, BaseComponent)
{
NsMeta<TypeId>("Person");
NsProp("Name", &Person::GetName, &Person::SetName);
NsProp("Weight", &Person::GetWeight, &Person::SetWeight);
}
};
class DataModel1: public NotifyPropertyChangedBase
{
public:
Person* GetPerson() const
{
return _person.GetPtr();
}
void SetPerson(Person* person)
{
if (_person != person)
{
_person.Reset(person);
OnPropertyChanged("Person");
}
}
private:
Ptr<Person> _person;
NS_IMPLEMENT_INLINE_REFLECTION(DataModel1, BaseComponent)
{
NsMeta<TypeId>("DataModel1");
NsProp("Person", &DataModel1::GetPerson, &DataModel1::SetPerson);
}
};
C#实现
在C#中,事情要容易得多。您基本上只需要实现INotifyPropertyChanged接口。
C#
public class Name: INotifyPropertyChanged
{
private string _first;
public string First
{
get { return _first; }
set
{
if (_first != value)
{
_first = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("First"));
}
}
}
}
private string _last;
public string Last
{
get { return _last; }
set
{
if (_last != value)
{
_last = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Last"));
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Person: INotifyPropertyChanged
{
private Name _name;
public Name Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
}
}
private float _weight;
public float Weight
{
get { return _weight; }
set
{
if (_weight != value)
{
_weight = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Weight"));
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class DataModel1
{
private Person _person;
public Person Person
{
get { return _person; }
set
{
if (_person != value)
{
_person = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Person"));
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
索引属性
在C#中,索引属性可以用作数据绑定的源。这样,您可以编写如下内容:
C#
class Person
{
public string Name {get; set;}
public string PhoneNumber {get; set;}
}
class Contacts
{
public IEnumerable<Person> Persons {get; set;}
public Person this[string Name]
{
get
{
return Persons.Where(p => p.Name == Name).FirstOrDefault();
}
}
}
<TextBox Text="{Binding Contacts[John].PhoneNumber}" />
注意
noesisGUI仅支持键类型为int或string的索引器。
字符串格式
如果需要格式化给定值的文本显示,则可以使用绑定声明中的StringFormat属性来格式化。使用StringFormat,您可以格式化信息输出的格式,而无需使用背后的代码或值转换器。
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
<DataModel1 x:Name="dataModel">
<DataModel1.Person>
<Person Weight="90">
<Person.Name>
<Name First="John" Last="Doe" />
</Person.Name>
</Person>
</DataModel1.Person>
</DataModel1>
</UserControl.Resources>
<StackPanel DataContext="{StaticResource dataModel}" HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="{Binding Person.Name.First}" />
<TextBlock Text="{Binding Person.Name.Last}" />
<TextBlock Text="{Binding Person.Weight, StringFormat=Weight is {0:F2}}" />
</StackPanel>
</UserControl>
StringFormat的更多示例:
<TextBlock Text="{Binding Amount, StringFormat={}{0:D}}" />
<TextBlock Text="{Binding Amount, StringFormat=Total: {0:D}}" />
<TextBlock Text="{Binding Amount, StringFormat=Total: {0:D} units}" />
注意
注意StringFormat属性后面的“ {}”吗?这样做是在'='符号后转义文本。这是必需的,因为'='符号后没有文本
标准数字格式字符串
格式说明符 | 名称 | 描述 | 例 |
---|---|---|---|
“ C”或“ c” | 货币 | 货币值 | 123.456(“ C”)➡$ 123,46 |
“ D”或“ d” | 小数 | 带可选负号的整数 | 1234(“ D”)➡1234 |
“ E”或“ e” | 指数的 | 指数通知 | 1052.0329112756(“ E”)➡1.052033E + 003 |
“ F”或“ f” | 固定点 | 带有负号的整数和十进制数字 | 234.567(“ F”)➡1234.57 |
“ G”或“ g” | 一般 | 定点数或科学记数法的紧凑性 | -123.456(“ G”)➡-123.456 |
“ N”或“ n” | 数 | 整数和十进制数字,组分隔符以及带可选负号的十进制分隔符 | 1234.567(“ N”)➡1,234.57 |
“ P”或“ p” | 百分 | 数字乘以100并显示百分号 | 1(“ P”)➡100.00% |
“ R”或“ r” | 往返 | 可以往返到相同数字的字符串 | 123456789.12345678(“ R”)➡123456789.12345678 |
“ X”或“ x” | 十六进制 | 十六进制字符串 | 255(“ X”)➡FF |
自定义数字格式字符串
格式说明符 | 名称 | 描述 | 例 |
---|---|---|---|
“ 0” | 零占位符 | 如果存在一个数字,则将零替换为相应的数字;否则,结果字符串中将出现零 | 1234.5678(“ 00000”)➡01235 |
“#” | 数字占位符 | 如果存在数字,则用相应的数字替换“#”符号;否则,结果字符串中不会出现数字 | 1234.5678(“ #####”)➡1235 |
“。” | 小数点 | 确定小数点分隔符在结果字符串中的位置 | 0.45678(“ 0.00”)➡0.46 |
“,” | 组分隔符和数字缩放 | 作为组分隔符和数字缩放说明符。作为组分隔符,它在每个组之间插入一个本地化的组分隔符。作为数字缩放说明符,它为指定的每个逗号将数字除以1000 |
2147483647(“ ##,#”)➡2,147,483,647
2147483647(“#,#,”)➡2,147
|
| “%” | 占位符百分比 | 将数字乘以100,然后在结果字符串中插入本地化的百分比符号 | 0.3697(“%#0.00”)➡%36.97 |
| “ E0” | 指数符号 | 如果后跟至少一个0(零),则使用指数表示法格式化结果。“ E”或“ e”的大小写指示结果字符串中指数符号的大小写。“ E”或“ e”字符后的零位数确定指数中的最小位数。加号(+)表示符号始终位于指数之前。减号(-)表示符号字符仅在负指数之前 |
987654(“#0.0e0”)➡98.8e4
1.8901385E-16(“ 0.0e + 00”)➡1.9e-16
1503.92311(“ 0.0 ## e + 00”)➡1.504e + 03
|
| “ \” | 转义符 | 使下一个字符被解释为文字而不是自定义格式说明符 | 87654(“ \ ### 00 \#”)➡#987654# |
| '串' | 文字字符串定界符 | 表示应将包含的字符原样复制到结果字符串中 | 68(“#'degrees'”)➡68度 |
| ; | 节分隔符 | 为正,负和零数字定义带有单独格式字符串的部分 | 12.345(“#0.0#;(#0.0#);-0-”)➡12.35 |
| 其他 | 所有其他字符 | 字符不变地复制到结果字符串 | 68(“#°”)➡68° |
绑定依赖项属性
依赖性属性具有内置的更改通知管道。此功能是noesisGUI保持目标属性和源属性同步的能力的关键。实现依赖项属性时,无需使用InotifyPropertyChange。实际上,如果您是从DependencyObject派生的,那么这应该是创建可绑定属性的首选方法。
依赖项属性是在可视元素后面的代码中强制创建的,例如:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="View2"
x:Name="root">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBox Text="{Binding ElementName=root, Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding ElementName=root, Path=Text}" Width="100" />
</StackPanel>
</UserControl>
如您所见,我们正在设置Binding的Mode属性。可以将其设置为BindingMode枚举的以下值之一:
DataBindingTutorialImg2.jpg- OneWay:只要源发生更改,目标就会更新。
- TwoWay:对目标或源的更改会更新另一个。
- OneWayToSource:所述的相对单向。每当目标更改时,源就会更新。
- OneTime:它与OneWay一样工作,除了对源所做的更改不会反映在目标上。在绑定启动时,目标保留源的快照。
TwoWay绑定适用于数据绑定表单,在该表单中,您的TextBox可能填充了允许用户更改的数据。实际上,尽管大多数依赖项属性默认为OneWay绑定,但是诸如TextBox.Text之类的依赖项属性默认为TwoWay绑定。
我们还使用UpdateSourceTrigger属性。使用TwoWay或OneWayToSource绑定时,您可能希望在何时以及如何更新源时使用不同的行为。例如,如果用户键入TwoWay数据绑定的TextBox,您是否希望每次击键都更新源,还是仅在用户完成输入后才更新源?通过绑定,您可以使用其UpdateSourceTrigger属性来控制此类行为。
可以将UpdateSourceTrigger设置为UpdateSourceTrigger枚举的成员,该枚举具有以下值:
- PropertyChanged:每当目标属性值更改时,源就会更新。
- LostFocus:当目标属性值更改时,仅在目标元素失去焦点之后才更新源。
- 显式:仅当您显式调用BindingExpression.UpdateSource时,才更新源。
正如不同的属性具有不同的默认模式设置一样,它们也具有不同的默认UpdateSourceTrigger设置。TextBox.Text默认为LostFocus。
C ++
class View2: public UserControl
{
public:
const char* GetText() const
{
return GetValue<NsString>(TextProperty).c_str();
}
void SetText(const char* text)
{
SetValue<NsString>(TextProperty, text);
}
static const DependencyProperty* TextProperty;
private:
NS_IMPLEMENT_INLINE_REFLECTION(View2, UserControl)
{
NsMeta<TypeId>("View2");
Ptr<UIElementData> data = NsMeta<UIElementData>(TypeOf<SelfClass>());
data->RegisterProperty<NsString>(TextProperty, "Text",
FrameworkPropertyMetadata::Create(NsString(""), FrameworkOptions_None));
}
};
C#
class View2: UserControl
{
public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
typeof(View2), new PropertyMetadata(""));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
}
绑定到收藏集
到目前为止,我们仅讨论了绑定到单个对象的情况,但是,绑定到数据集合是一种常见的情况。例如,一种常见的情况是使用诸如ListBox,ListView或TreeView之类的ItemsControl来显示数据集合。
创建具有ListBox.Items作为目标属性的Binding是有意义的,但是,可惜,Items不是依赖项属性。但是ListBox(以及所有其他ItemsControl)具有ItemsSource依赖项属性,该属性专门用于此数据绑定方案。
image.png<ListBox ItemsSource="{Binding Source={StaticResource items}}" />
为了使目标属性保持对源集合的更改进行更新,源集合必须实现一个称为INotifyCollectionChanged的接口。幸运的是,noesisGUI已经有一个内置类可以为您完成此工作。它称为ObservableCollection。
<Grid
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="600">
<Grid.Resources>
<DataModel3 x:Name="dataModel" />
<DataTemplate x:Key="TaskTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<Rectangle Width="15" Height="10" Fill="{Binding Color}" Stroke="Transparent"
StrokeThickness="0" Margin="5, 0, 5, 0" Grid.Column="0"/>
<TextBlock Text="{Binding Name}" Margin="5, 0, 5, 0" Grid.Column="1"/>
<TextBlock Text="{Binding Scale, StringFormat=P0}" Margin="5, 0, 5, 0" Grid.Column="2"/>
<TextBlock Text="{Binding Pos}" Margin="5, 0, 5, 0" Grid.Column="3"/>
</Grid>
</DataTemplate>
</Grid.Resources>
<ListBox Height="100" DataContext="{StaticResource dataModel}"
ItemsSource="{Binding Players}"
ItemTemplate="{StaticResource TaskTemplate}" />
</Grid>
请注意,我们使用ItemTemplate属性来控制每个项目的呈现方式。此属性设置为DataTemplate的实例。DataTemplate从FrameworkTemplate派生。因此,它具有VisualTree内容属性,可以将其设置为FrameworkElements的任意树。
注意
对于非常简单的情况,可以使用所有ItemsControls上的DisplayMemberPath属性。此属性与ItemsSource协同工作。如果将其设置为适当的属性路径,则将为每个项目呈现相应的属性值。
应用数据模板时,会隐式地为其提供适当的数据上下文。当用作ItemTemplate时,数据上下文隐式为ItemsSource中的当前项目。
DataBindingTutorialImg6.jpgC ++
class Player: public BaseComponent
{
public:
Player() {}
Player(NsString name, Color color, float scale, NsString pos) : _name(name), _scale(scale),
_pos(pos), _color(*new SolidColorBrush(color)) {}
private:
NsString _name;
float _scale;
NsString _pos;
Ptr<Brush> _color;
NS_IMPLEMENT_INLINE_REFLECTION(Player, BaseComponent)
{
NsMeta<TypeId>("Player");
NsProp("Name", &Player::_name);
NsProp("Scale", &Player::_scale);
NsProp("Pos", &Player::_pos);
NsProp("Color", &Player::_color);
}
};
class DataModel3: public BaseComponent
{
public:
DataModel3()
{
_players = *new ObservableCollection<Player>;
Ptr<Player> player0 = *new Player("Player0", Color::Red, 1.0f, "(0,0,0)");
_players->Add(player0);
Ptr<Player> player1 = *new Player("Player1", Color::Gray, 0.75f, "(0,30,0)");
_players->Add(player1);
Ptr<Player> player2 = *new Player("Player2", Color::Orange, 0.50f, "(0,-10,0)");
_players->Add(player2);
Ptr<Player> player3 = *new Player("Player3", Color::Green, 0.85f, "(0,-10,0)");
_players->Add(player3);
}
private:
Ptr<ObservableCollection<Player>> _players;
NS_IMPLEMENT_INLINE_REFLECTION(DataModel3, BaseComponent)
{
NsMeta<TypeId>("DataModel3");
NsProp("Players", &DataModel3::_players);
}
};
C#
public class Player
{
public Player(string name, Color color, float scale, string pos)
{
Name = name;
Color = new SolidColorBrush(color);
Scale = scale;
Pos = pos;
}
public string Name { get; private set; }
public Brush Color { get; private set; }
public float Scale { get; private set; }
public string Pos { get; private set; }
}
public class DataModel3
{
public DataModel3()
{
Players = new ObservableCollection<Player>();
Players.Add(new Player("Player0", Colors.Red, 1.0f, "(0,0,0)"));
Players.Add(new Player("Player1", Colors.Gray, 0.75f, "(0,30,0)"));
Players.Add(new Player("Player2", Colors.Orange, 0.50f, "(0,-10,0)"));
Players.Add(new Player("Player3", Colors.Green, 0.85f, "(0,-10,0)"));
}
public ObservableCollection<Player> Players { get; private set; }
}
存在一个特殊的DataTemplate子类,用于处理分层数据。此类称为HierarchicalDataTemplate。它不仅使您能够更改此类数据的表示形式,而且还使您可以将对象的层次结构直接绑定到本质上理解层次结构的元素,例如TreeView或Menu控件。
想法是对层次结构中的每种数据类型使用HierarchicalDataTemplate,然后对任何叶节点使用简单的DataTemplate。每个数据模板都允许您自定义数据类型的呈现方式,但是HierarchicalDataTemplate还使您可以通过设置其ItemsSource属性来在层次结构中指定其子级。
DataBindingTutorialImg7.jpg<Grid
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.Resources>
<LeagueList x:Key="items" />
<HierarchicalDataTemplate DataType="League" ItemsSource="{Binding Path=Divisions}">
<TextBlock Foreground="LightCyan" Text="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="Division" ItemsSource="{Binding Path=Teams}">
<TextBlock Foreground="Snow" Text="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
<DataTemplate DataType="Team">
<TextBlock Foreground="Moccasin" Text="{Binding Path=Name}"/>
</DataTemplate>
</Grid.Resources>
<TreeView DataContext="{StaticResource items}">
<TreeViewItem ItemsSource="{Binding Leagues}" Header="My Soccer Leagues" />
</TreeView>
</Grid>
价值转换器
数据模板可以通过呈现某些目标值的方式进行自定义,而值转换器可以将源值转换为完全不同的目标值。它们使您可以插入自定义逻辑,而不会放弃数据绑定的好处。
值转换器通常用于协调不同数据类型的源和目标。例如,您可以在某些非Brush数据源的值上更改元素的背景或前景色。或者,您可以使用它来简单地增强显示的信息,而无需单独的元素,例如在原始计数中添加“ item”后缀。
DataBindingTutorialImg4.jpg例如,以下XAML 使用名为BooleanToVisibilityConverter的转换器,根据CheckBox的IsChecked属性切换元素的可见性。
<Grid
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="Gray">
<Grid.Resources>
<BooleanToVisibilityConverter x:Key="converter"/>
</Grid.Resources>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<CheckBox x:Name="checkBox" Content="Show Button" Width="100"/>
<Button Content="Click" Margin="5" Visibility="{Binding ElementName=checkBox, Path=IsChecked,
Converter={StaticResource converter}}"/>
</StackPanel>
</Grid>
您可以通过继承BaseValueConverter来创建自己的转换器。扩展教程中显示了一个示例。
太阳系实例
DataBindingTutorialImg5.jpg本示例总结了本教程中介绍的所有概念。它基本上由一个绑定到SolarSystemObject项目的ObservableCollection的ListBox组成。
<ListBox ItemsSource="{Binding Source={StaticResource solarSystem}, Path=SolarSystemObjects}" />
每个行星的外观由DataTemplate定义,该DataTemplate使用数据绑定和转换器生成最终外观。
注意
此示例改编自WPF中样式和模板的功能
网友评论