WPF桌面端开发-MVVM编程之数据绑定(Binding)

前言

在WPF中,Binding是一种强大的机制,用于将数据源与目标对象(通常是UI控件)进行绑定。

以下是一些常用的Binding属性及其说明:

Source: 指定数据源对象,可以是一个对象实例、属性路径、静态资源或者父级元素的属性。

1
<TextBox Text="{Binding Path=UserName, Source={StaticResource myDataSource}}" />

Path: 指定数据源中的属性路径,告诉绑定系统从哪里获取数据。

1
<TextBox Text="{Binding Path=UserName}" />

Mode: 指定绑定的方向和方式,例如单向绑定、双向绑定、或单向到源的绑定。

1
<TextBox Text="{Binding Path=UserName, Mode=TwoWay}" />

Converter: 提供一个转换器,用于在绑定源和绑定目标之间进行转换。

1
<TextBox Text="{Binding Path=OrderTotal, Converter={StaticResource priceConverter}}" />

UpdateSourceTrigger: 指定何时更新数据源的值,例如在属性更改时立即更新,或在控件失去焦点时更新。

1
<TextBox Text="{Binding Path=UserName, UpdateSourceTrigger=PropertyChanged}" />

FallbackValue: 当绑定的值无法获取时,提供一个备用的默认值。

1
<TextBox Text="{Binding Path=OrderTotal, FallbackValue=0}" />

TargetNullValue: 当目标绑定值为null时显示的值。

1
<TextBox Text="{Binding Path=OrderTotal, TargetNullValue=Not available}" />

NotifyOnSourceUpdatedNotifyOnTargetUpdated: 用于指定在绑定源或绑定目标更新时是否发出通知。

1
<TextBox Text="{Binding Path=UserName, NotifyOnTargetUpdated=True}" />

IsAsync: 指定是否异步处理绑定,适用于长时间运行的操作。

1
<TextBox Text="{Binding Path=UserName, IsAsync=True}" />

ValidatesOnDataErrors, ValidatesOnExceptionsNotifyOnValidationError: 用于指定在数据验证失败时是否发出通知,并且如何处理数据验证和异常。

1
<TextBox Text="{Binding Path=UserName, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" />

这些属性可以按照需要组合使用,以便精确地控制WPF中的数据绑定行为。

作用域

当你在XAML中使用Binding时,你可以指定目标属性的作用域,通常有以下几种情况:

  1. 默认作用域

    如果你没有显式指定作用域,Binding 将在当前 DataContext 的上下文中进行搜索。

    这意味着它会尝试在父元素的 DataContext 中查找绑定的属性。

  2. ElementName 绑定

    通过设置 Binding 的 ElementName 属性,你可以指定一个具体的元素来作为绑定的目标,Binding 将在该元素的上下文中搜索属性。

  3. RelativeSource 绑定

    通过设置 Binding 的 RelativeSource 属性,你可以指定绑定相对于其他元素的位置,比如指定为父元素或者某个特定类型的元素。

  4. Source 绑定

    通过设置 Binding 的 Source 属性,你可以直接指定一个数据源对象,Binding 将在该对象的上下文中搜索属性。

默认作用域

如果你没有显式指定作用域,Binding 将在当前 DataContext 的上下文中进行搜索。

这意味着它会尝试在父元素的 DataContext 中查找绑定的属性。

1
2
3
4
<TextBlock
Margin="0,2,0,0"
HorizontalAlignment="Center"
Text="{Binding Name}"/>

ElementName 绑定

简单示例

1
2
3
4
<Rectangle Fill="Red" Name="rectangle"
Height="100" Stroke="Black"
Canvas.Top="100" Canvas.Left="100"
Width="{Binding ElementName=rectangle,Path=Height}"/>

或者

1
2
3
4
<StackPanel>
<TextBox x:Name="textBox1" Text="{Binding Path=Value,ElementName=slider1}"/>
<Slider x:Name="slider1" Maximum="100" Minimum="0"/>
</StackPanel>

自定义组件

自定义组件没有显式指定作用域时,默认作用域不是DataContext。

1
2
3
4
5
6
7
8
9
10
11
<DataTemplate x:Key="BlackbordTpl" DataType="page:SubmitUserItem">
<Border
Name="OuterBorder"
BorderBrush="{Binding Path=Selected, Converter={StaticResource ZGenericTypeConverter}, ConverterParameter='true:#339DFF|other:#00ffffff'}"
BorderThickness="2"
Focusable="True"
PreviewMouseDown="UIElement_OnMouseDown"
Tag="{Binding}">
<uc:UcBlackbord ShowToolbar="{Binding DataContext.Selected, ElementName=OuterBorder}" />
</Border>
</DataTemplate>

RelativeSource 绑定

绑定自身属性

正方形

1
2
3
<Rectangle Fill="Red" Height="100"
Stroke="Black"
Width="{Binding RelativeSource={RelativeSource Self},Path=Height}"/>

绑定祖先元素属性

没有直接绑定父组件的,绑定父组件使用这种方法实现。

如果想让图片上下填充满,左右等比自适应,可以使用下面的方式实现:

1
2
3
4
5
6
7
8
9
10
<Border BorderBrush="#434343" BorderThickness="1">
<Grid>
<Image
Height="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType={x:Type Grid}}}"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Source="{Binding}"
Stretch="Uniform" />
</Grid>
</Border>

这里

设置图片的高度后缩放方式设置为Uniform,这样图片高度就固定了,宽度会等比缩放,再设置水平居中,就实现了这个效果。

其中

RelativeSource={RelativeSource AncestorType={x:Type Grid}} 是用来指定查找祖先中最近类型为Grid的元素。

TemplatedParent

此模式允许将给定的 ControlTemplate 属性绑定到应用 ControlTemplate 的控件的属性。

为了更好地理解这里的问题,下面是一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<Window.Resources>
<ControlTemplate x:Key="ZTemplate">
<Canvas>
<Canvas.RenderTransform>
<RotateTransform Angle="20" />
</Canvas.RenderTransform>
<Ellipse
Width="150"
Height="100"
Fill="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Background}" />
<ContentPresenter Margin="35" Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}" />
</Canvas>
</ControlTemplate>
</Window.Resources>
<Canvas Name="Parent0">
<Button
Canvas.Left="0"
Canvas.Top="0"
Width="0"
Height="0"
Margin="50"
Template="{StaticResource ZTemplate}">
<TextBlock FontSize="22">Click me</TextBlock>
</Button>
</Canvas>

如果我想应用给定控件的属性到它的控件模板,那么我可以使用TemplatedParent模式。

TemplateBinding一般用于绑定控件模板内的属性,而TemplatedParent用于在控件模板内访问父元素的属性。

TemplateBinding

在 WPF 中, TemplateBinding 用于在控件模板中绑定到控件的属性。这可以让模板基于控件的属性值更改其视觉体验。

这里是一个简单示例:MainWindow.xaml

1
2
3
4
5
6
7
8
9
10
11
<Window x:Class="TemplateBindingDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TemplateBindingDemo"
Title="MainWindow" Height="350" Width="525">
<Grid>
<local:MyButton Template="{StaticResource MyButtonTemplate}"
Background="Blue"
Content="Button" />
</Grid>
</Window>

MyButton.xaml

1
2
3
4
5
6
7
8
9
10
11
12
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TemplateBindingDemo">

<ControlTemplate x:Key="MyButtonTemplate" TargetType="local:MyButton">
<Border BorderBrush="Black" BorderThickness="1"
Background="{TemplateBinding Background}">
<ContentPresenter Content="{TemplateBinding Content}" />
</Border>
</ControlTemplate>
</ResourceDictionary>

MyButton.cs

1
2
3
4
public class MyButton : Button 
{
// ...
}

在这里,我们为 MyButton 定义了一个 ControlTemplate。

在模板中,我们使用 {TemplateBinding Background}{TemplateBinding Content} 来绑定到控件的 BackgroundContent 属性。
所以模板会随着控件属性的改变而改变。

运行这个示例,你会看到一个蓝色的按钮,上面写着 “Button”。如果你改变 MyButtonBackgroundContent 属性,模板会相应更新。

Source 绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- MainWindow.xaml -->
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
Title="MainWindow" Height="450" Width="800">

<Window.Resources>
<local:MyViewModel x:Key="ViewModel" />
</Window.Resources>

<Grid>
<TextBox Text="{Binding Source={StaticResource ViewModel}, Path=TextValue}" />
</Grid>
</Window>

定义的ViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// MyViewModel.cs
using System.ComponentModel;

public class MyViewModel : INotifyPropertyChanged
{
private string _textValue;

public string TextValue
{
get { return _textValue; }
set
{
if (_textValue != value)
{
_textValue = value;
OnPropertyChanged(nameof(TextValue));
}
}
}

public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

可绑定的值

可用来绑定的两种类型

  • 使用依赖属性
  • 实现INotifyPropertyChanged接口

依赖属性

示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public partial class UcBlackbord
{
//UserControl中接收值的属性
public static readonly DependencyProperty ShowToolbarProperty = DependencyProperty.Register(
nameof(ShowToolbar),
typeof(bool),
typeof(UcBlackbord),
new PropertyMetadata(false)
);

public bool ShowToolbar
{
get => (bool)GetValue(ShowToolbarProperty);
set => SetValue(ShowToolbarProperty, value);
}
}

使用

1
Visibility="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:UcBlackbord}}, Path=ShowToolbar, Converter={StaticResource BooleanToVisibilityConverter}}"

示例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public partial class UcTimeNum : UserControl
{
public static readonly DependencyProperty NumProperty = DependencyProperty.Register(
nameof(Num),
typeof(int),
typeof(UcTimeNum),
new PropertyMetadata(0)
);

public int Num
{
get => (int)GetValue(NumProperty);
set => SetValue(NumProperty, value);
}
}

页面

1
2
3
<TextBlock
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:UcTimeNum}}, Path=Num}">
</TextBlock>

实现INotifyPropertyChanged接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyViewModel : INotifyPropertyChanged
{
private string _textValue;

public string TextValue
{
get { return _textValue; }
set
{
if (_textValue != value)
{
_textValue = value;
OnPropertyChanged(nameof(TextValue));
}
}
}

public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

绑定属性的查找机制

  1. 数据上下文(DataContext):如果在自定义控件内部使用数据绑定,默认情况下,绑定会从控件的 DataContext 开始查找。

    如果控件的 DataContext 设置了某个对象,那么绑定就会尝试在这个对象上查找绑定的属性。

  2. 绑定路径(Binding Path):绑定路径决定了属性查找的方式。

    比如,如果你在自定义控件中这样设置绑定:

    1
    <TextBlock Text="{Binding MyProperty}" />

    这里的 MyProperty 会在控件的 DataContext 中查找。

  3. RelativeSource:有时候,绑定可能需要在控件的容器或其它上下文中查找属性。

    可以通过 RelativeSource 来实现这一点。

    例如:

    1
    <TextBlock Text="{Binding DataContext.MyProperty, RelativeSource={RelativeSource AncestorType=Page}}" />

    这个绑定会在当前页面的 DataContext 中查找 MyProperty

控件绑定属性从内部查找的原因

默认的 DataContext

如果自定义控件的 DataContext 已经被设置为某个对象,并且控件内部的绑定没有指定路径的源,那么绑定会在控件的 DataContext 中查找。

属性的优先级

控件的 DataContext 优先于控件容器的 DataContext,除非你明确指定了绑定的来源。

解决方案

明确指定绑定源:如果希望绑定从控件的容器中查找属性,可以使用 RelativeSourceElementName 来明确指定绑定的源。

例如:

1
<TextBlock Text="{Binding ElementName=MyPage, Path=DataContext.MyProperty}" />

数据绑定注意点

  1. 在给组件赋值的时候我们只能在UI线程中处理,同样也只能在UI线程中对DataContext已绑定的对象进行操作。
  2. 文本组件可以绑定int和double类型,不用必须为string类型。

另外提一下

我们在做JSON转换的时候,数字类型的值可能为空的时候,直接转对象必须属性为可空类型。

有时候为了方便处理,我们可以把属性定义为string,这样也是能成功转换的。

设置上下文

组件中

在WPF中,通过d:DataContext属性可以为设计时数据绑定设置数据上下文,这在开发和设计阶段非常有用,可以代码提醒和点击跳转到对应属性中。

指定数据上下文类

如果你有一个名为 YourViewModel 的类作为数据上下文,可以在 d:DataContext 中引用它,以便在设计时绑定界面元素的数据。

示例

1
2
3
4
5
6
7
<Window ...
xmlns:local="clr-namespace:YourNamespace"
d:DataContext="{d:DesignInstance local:YourViewModel}">
<Grid>
<!-- 这里可以使用设计时数据绑定到 YourViewModel 中的属性 -->
</Grid>
</Window>

注意事项:

  • 命名空间:确保在使用d:DataContext时正确引用你的ViewModel类的命名空间。
  • 仅限设计时d:DataContext仅用于设计时数据绑定,不影响运行时的实际数据绑定和行为。
  • 设计时数据源:这允许在没有运行时数据源的情况下预览和调试界面。

DataTemplate中

在DataTemplate中使用DataType指定

1
2
3
<DataTemplate x:Key="TjItemDt"
DataType="{x:Type page:VoteTj}">
</DataTemplate>

数据绑定

定义基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System.ComponentModel;

namespace Z.Common
{
public class ZNotifyModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

public void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}

定义数据源的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ToolbarModel : ZNotifyModel
{
public ObservableCollection<ToolbarMenu> menuList { get; set; }
bool _IsRight = true;
public bool IsRight
{
get { return _IsRight; }
set { _IsRight = value; OnPropertyChanged("IsRight"); }
}
public ToolbarModel()
{
menuList = new ObservableCollection<ToolbarMenu>();
}
}

public class ToolbarMenu : ZNotifyModel
{
string _name;
public string Name
{
get { return _name; }
set { _name = value; OnPropertyChanged("Name"); }
}

public string Pic { get; set; }
}

上面例子中我们可以看到,

如果我们要在数据改变时通知页面改变的属性都要在Set方法中调用OnPropertyChanged

而列表不再用List,而是使用ObservableCollection

ObservableCollection

注意ObservableCollection中调用Add方法会触发列表刷新,但是如果直接更换了对象就不会刷新了。

方式1

如下示例

1
2
3
4
5
6
7
8
9
10
int pageSize = 15;
int totalPage = (int)Math.Ceiling(1.0 * this._pageData.list.Count / pageSize);
if (totalPage == 0)
{
totalPage = 1;
}
this._pageData.totalPage = totalPage;
int startIndex = (this._pageData.currPage - 1) * pageSize;
IEnumerable<JiandaDetailUser> jiandaDetailUsers = this._pageData.list.Skip(startIndex).Take(pageSize);
this._pageData.pageList = new ObservableCollection<JiandaDetailUser>(jiandaDetailUsers);

其中pageList必须调用OnPropertyChanged方法才会刷新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class JianDaDetailPageData : ZNotifyModel
{
public ObservableCollection<JiandaDetailUser> list { get; set; }

private ObservableCollection<JiandaDetailUser> _pageList;

public ObservableCollection<JiandaDetailUser> pageList
{
get => _pageList;
set
{
_pageList = value;
OnPropertyChanged("pageList");
}
}
}

方式2

如果pageList保持原样那么赋值只能使用Add方法

1
2
3
4
5
this._pageData.pageList.Clear();
foreach (JiandaDetailUser jiandaDetailUser in jiandaDetailUsers)
{
this._pageData.pageList.Add(jiandaDetailUser);
}

对应

1
2
3
4
public class JianDaDetailPageData : ZNotifyModel
{
public ObservableCollection<JiandaDetailUser> pageList { get; set; }
}

设置数据源

代码中也要进行数据源的设置

1
2
3
4
5
6
7
pageData.IsRight = true;
pageData.menuList.Add(new ToolbarMenu()
{
Name = "开始直播",
Pic = "Images/ToolBar/toobar_12_1.png"
});
DataContext = pageData;

上面设置整个页面的数据,当然也可以设置某个组件的数据源

1
this.toolbar_list.DataContext = mydata;

页面中绑定值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<Window.Resources>
<DataTemplate x:Key="ToolbarMenu">
<Button x:Name="toolbar_item" Background="Transparent" BorderThickness="0" Cursor="Hand" Height="60" Click="toolbar_item_Click">
<Button.Content>
<StackPanel Width="Auto" Background="Transparent">
<Image HorizontalAlignment="Center" Width="44" Source="{Binding Pic}"/>
<TextBlock HorizontalAlignment="Center" Text="{Binding Name}" Foreground="#3C525B"/>
</StackPanel>
</Button.Content>
</Button>
</DataTemplate>
</Window.Resources>
<ItemsControl
x:Name="toolbar_list"
ItemsSource="{Binding menuList}"
ItemTemplate="{StaticResource ToolbarMenu}"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
Grid.Row="1" Background="#f3f3f3" BorderThickness="0">
</ItemsControl>

双向绑定

1
2
<TextBox Grid.Row="0" Text="{Binding Title,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}"/>
<TextBlock Grid.Row="0" Text="{Binding Path=Title}"/>

关键属性 UpdateSourceTrigger=PropertyChanged,Mode=TwoWay

转换器

https://www.psvmc.cn/article/2019-12-30-wpf-start-03-converter.html

自定义代码片段

上面属性写的时候比较麻烦,建议使用自定义代码片段。

Visual Studio创建

在任意地方创建一个文件夹,最好是你不去经常移动的地方,文件夹是用来存放你自定义的代码块的文件夹,

我就创建了一个名称:csharp_snippet 的文件夹

新建代码块文件zprop.snippet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>zprop</Title>
<Shortcut>zprop</Shortcut>
<Description>自动实现的属性的代码片段</Description>
<Author>剑行者</Author>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>type</ID>
<ToolTip>属性类型</ToolTip>
<Default>string</Default>
</Literal>
<Literal>
<ID>property</ID>
<ToolTip>属性名</ToolTip>
<Default>Name</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[
$type$ _m$property$;
public $type$ $property$
{
get { return _m$property$; }
set { _m$property$ = value; OnPropertyChanged("$property$"); }
}
$end$]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>

保存啦,然后依然是去 工具–>代码段管理器 –>选择Visual C#语言 –>选择下方的添加 –>浏览到你自定义的那个放代码块的文件夹就OK啦。

重启开发工具。

此时要我在项目中打出zprop按两次Tab 那我的数据访问层的代码就全部出来啦,当然还要添加一些引用就可以啦

ReSharper插件创建

安装ReSharper插件后,所有自定义的代码段都失效了,是因为

安装ReSharper插件后,它会自动导入代码段,但是后来的代码段是不会被导入的,只能我们自己添加。

配置步骤查看:

https://www.psvmc.cn/article/2019-12-27-resharper-config.html