WPF Binding 绑定

绑定是 wpf 开发中的精髓,有绑定才有所谓的数据驱动。

1 . 背景

目前 wpf 界面可视化的控件,继承关系如下,

控件的数据绑定,基本上都要借助于 FrameworkElement 的 DataContext 属性。

只有先设置了控件的 DataContext 属性,再设置绑定才能生效(TemplateBinding、使用 RelativeSource、后台 new Binding、静态资源等特殊情况另外讨论)。

2. 绑定的参与对象

绑定过程涉及到两个重要的对象

  1. 绑定目标

数据源绑定到的 依赖属性,只有依赖属性才能作为绑定目标。例如 TextBlock.TextPropertyItemsControl.ItemsSourceProperty

不是只有控件才有依赖属性,普通 CLR 类也可以包括依赖属性。

  1. 绑定源

依赖属性绑定的数据源,例如类中的 属性、界面中的 资源 等。

3. 使用 DataContext 的数据绑定

定义一个包含 2 个只读属性的类型,

csharp 复制代码
public class SampleViewModel
{
    public string Title { get; }
    public int Count { get; }

    public SampleViewModel()
    {
        Title = "Hello~";
        Count = 100;
    }
}

界面以 UserControl 为例,在构造函数中设置整个界面的 DataContext。

DataContext 会自动从父类向子类传递,子元素也可重新设置 DataContext 属性为其它值。

设置界面控件的 DataContext,

csharp 复制代码
public partial class BindingView : UserControl
{
    public BindingView()
    {
        InitializeComponent();

        DataContext = new SampleViewModel();
    }
}

3.1 在前端代码中设置绑定

xml 复制代码
<UserControl
    x:Class="WpfApp1.Views.BindingView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:viewModels="clr-namespace:WpfApp1.ViewModels"
    d:DataContext="{d:DesignInstance viewModels:SampleViewModel}"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <Grid Background="DeepPink" Width="100" Height="100">
        <StackPanel>
            <TextBlock Margin="4" Text="{Binding Title}" />
            <TextBlock Margin="4" Text="{Binding Count}" />
        </StackPanel>
    </Grid>
</UserControl>

显示结果,

3.2 在后端代码中设置绑定

将前端绑定代码去掉,并设置控件的 x:Name 属性值,

xml 复制代码
<Grid Background="DeepPink" Width="100" Height="100">
    <StackPanel>
        <TextBlock x:Name="Text1" Margin="4" />
        <TextBlock x:Name="Text2" Margin="4" />
    </StackPanel>
</Grid>

在后端中手动绑定,

csharp 复制代码
public BindingView()
{
    InitializeComponent();

    DataContext = new SampleViewModel();

	//后台代码绑定
    var binding1 = new Binding("Title")
    {
        Source = this.DataContext,
    };
    var binding2 = new Binding("Count")
    {
        Source = this.DataContext,
    };

    this.Text1.SetBinding(TextBlock.TextProperty, binding1);
    this.Text2.SetBinding(TextBlock.TextProperty, binding2);
}

同样可以实现绑定。

上面的例子,绑定参与对象是:

绑定目标:TextBlock.TextProperty 依赖属性

绑定源:this.DataContext 即 SampleViewModel 实例

绑定路径:TitleCount

也可以简单点说,

绑定目标:TextBlock.TextProperty 依赖属性

绑定源: SampleViewModel 实例的 TitleCount 属性

一般推荐直接在前端绑定,有特殊业务需求才会在后台动态绑定。

4. 不使用 DataContext 的数据绑定

不设置 DataContext 的情况下, 也可以使用绑定,比如绑定界面控件或控件的属性、页面资源等。

xml 复制代码
xmlns:system="clr-namespace:System;assembly=mscorlib"
xml 复制代码
<UserControl.Resources>
    <system:String x:Key="Content">我是静态资源</system:String>
</UserControl.Resources>

<Grid Background="DeepPink" Width="100" Height="100">
    <StackPanel>
        <!-- 绑定 CLR 属性 -->
        <TextBlock x:Name="Text1" Margin="4" Text="{Binding Title}" />

        <!-- 绑定控件 -->
        <TextBlock x:Name="Text2" Margin="4" Text="无" Tag="{Binding ElementName=Text1}" />

        <!-- 绑定控件属性 -->
        <TextBlock x:Name="Text3" Margin="4" Text="{Binding ElementName=Text2, Path=Tag.Text}" />

        <!-- 绑定页面资源 -->
        <TextBlock x:Name="Text4" Margin="4" Text="{Binding ., Source={StaticResource Content}}" />
    </StackPanel>
</Grid>

显示,

总结,

绑定目标必须是依赖属性,绑定源可以是依赖属性也可以是普通 CLR 属性、静态资源。

5. 绑定模式

以上演示的是简单的数据绑定,仅仅是用来显示,如果我还想把界面数据传回数据源,该如何操作?

以 CheckBox 勾选框为例,

xml 复制代码
<Grid Background="DeepPink" Width="100" Height="100">
    <CheckBox Margin="4" Content="Mark" IsChecked="{Binding Mark}" />
</Grid>

绑定源:

csharp 复制代码
public class BindingModeViewModel
{
    private bool _mark = true; //默认勾选
    public bool Mark
    {
        get => _mark;
        set
        {
            if (_mark != value)
            {
                _mark = value;
                Console.WriteLine($"Mark changed to {_mark}");
            }
        }
    }
}

取消勾选并再次勾选,结果,

bash 复制代码
Mark changed to False
Mark changed to True

接下来对控件绑定做一些改动,

xml 复制代码
<CheckBox Margin="4" Content="Mark" IsChecked="{Binding Mark, Mode=OneWay}" />

发现操作 CheckBox,不再会打印信息了。那是因为设置绑定模式 Mode=OnWay 就表示只支持从数据源到界面目标的单向绑定。

绑定模式有如下几种,

csharp 复制代码
public enum BindingMode
{
  TwoWay, //双向绑定
  OneWay, //单向绑定:从数据源到目标
  OneTime, //单次绑定:只绑定一次
  OneWayToSource, //单向绑定:从目标到数据源
  Default, //默认绑定:根据数据源和目标自动决定
}

所以,这里我把 Mode 设置为 TwoWay、OneWayToSource、Default(不设置就是默认绑定)都可以打印出来信息。

到此,演示了:

  1. 从数据源获取数据到目标(如 TextBlock)
  2. 从目标设置数据到数据源(如 CheckBox)

那如果,我要从数据源设置动态数据到目标,该如何操作?这就要用到一个接口。

6. INotifyPropertyChanged 接口

csharp 复制代码
public interface INotifyPropertyChanged
{
  event PropertyChangedEventHandler PropertyChanged;
}

INotifyPropertyChanged 是一个接口,用于在属性值发生变化时通知绑定的客户端。在 WPF 应用程序中,这通常用于实现数据绑定,使得 UI 元素能够在源数据动态变化时自动更新。

简单实现,

csharp 复制代码
public class MyNotifyClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void RaisePropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

改动一下数据源,

csharp 复制代码
public class BindingModeViewModel : MyNotifyClass
{
    private bool _mark = true;
    private string _printMessage;
    
    public bool Mark
    {
        get => _mark;
        set
        {
            if (_mark != value)
            {
                _mark = value;

                PrintMessage = $"Mark changed to {_mark}";
            }
        }
    }

    public string PrintMessage
    {
        get => _printMessage;
        set
        {
            _printMessage = value;
            RaisePropertyChanged(nameof(PrintMessage));
        }
    }
}

调整一下界面,

xml 复制代码
<Grid Background="DeepPink" Width="200" Height="100">
    <StackPanel>
        <CheckBox Margin="4" Content="Mark" IsChecked="{Binding Mark}" />
        <TextBlock Margin="4" Text="{Binding PrintMessage}" />
    </StackPanel>
</Grid>

点击 CheckBox , TextBlock 会动态显示信息。

CheckBox 只是通过绑定属性,利用属性 Set 方式触发数据源变化。

如果要像按钮 Button 那样,直接通过方法来触发数据源变化,如果操作呢?就要用到命令绑定。

7. 命令绑定

在 WPF 中,命令绑定是 MVVM 模式的核心部分之一,它允许开发者将视图中的事件如按钮点击与 ViewModel 中的命令逻辑相绑定。这样,当用户界面上的按钮被点击时,可以触发 ViewModel 中定义的命令执行相应的操作,而不需要在代码后面编写事件处理函数。

以按钮 Button 控件为例,按钮的 Command 属性类型是一个 ICommand 接口类型,

csharp 复制代码
public ICommand Command
{
  get => (ICommand) this.GetValue(ButtonBase.CommandProperty);
  set => this.SetValue(ButtonBase.CommandProperty, (object) value);
}
csharp 复制代码
  public interface ICommand
  {
    /// <summary>当出现影响是否应执行该命令的更改时发生。</summary>
    event EventHandler CanExecuteChanged;

    /// <summary>定义确定此命令是否可在其当前状态下执行的方法。</summary>
    /// <param name="parameter">此命令使用的数据。  如果此命令不需要传递数据,则该对象可以设置为 <see langword="null" />。</param>
    /// <returns>如果可执行此命令,则为 <see langword="true" />;否则为 <see langword="false" />。</returns>
    bool CanExecute(object parameter);

    /// <summary>定义在调用此命令时要调用的方法。</summary>
    /// <param name="parameter">此命令使用的数据。  如果此命令不需要传递数据,则该对象可以设置为 <see langword="null" />。</param>
    void Execute(object parameter);
  }

因此我们只要实现一下 ICommand 接口即可使用自己的命令。

例如,

csharp 复制代码
public class MyCommand : ICommand
{
    private readonly Action<object> _executeAction;
    private readonly Func<object, bool> _canExecuteFunc;

    public event EventHandler CanExecuteChanged;

    public MyCommand(Action<object> executeAction, Func<object, bool> canExecuteFunc = null)
    {
        _executeAction = executeAction;
        _canExecuteFunc = canExecuteFunc;
    }

    public void Execute(object parameter)
    {
        _executeAction?.Invoke(parameter);
    }

    public bool CanExecute(object parameter)
    {
        return _canExecuteFunc == null || _canExecuteFunc(parameter);
    }
    
	public void RaiseCanExecute()
	{
	    CanExecuteChanged?.Invoke(this, EventArgs.Empty);
	}
}

在数据源增加相应的命令属性,

csharp 复制代码
public class BindingModeViewModel : MyNotifyClass
{
    private bool _mark = true;
    private string _printMessage;
    public bool Mark
    {
        get => _mark;
        set
        {
            if (_mark != value)
            {
                _mark = value;

                PrintMessage = $"Mark changed to {_mark}";

                //更新按钮可点击状态
                PrintCommand.RaiseCanExecute();
            }
        }
    }

    public string PrintMessage
    {
        get => _printMessage;
        set
        {
            _printMessage = value;
            RaisePropertyChanged(nameof(PrintMessage));
        }
    }

    public MyCommand PrintCommand { get; }

    public BindingModeViewModel()
    {
        PrintCommand = new MyCommand(Print, CanPrint);
    }

    private void Print(object obj)
    {
        PrintMessage = "This is Button Print.";
    }

    private bool CanPrint(object arg)
    {
        return _mark;
    }
}

界面增加一个按钮,

xml 复制代码
<Grid Background="DeepPink" Width="200" Height="100">
    <StackPanel>
        <CheckBox Margin="4" Content="Mark" IsChecked="{Binding Mark}" />
        <TextBlock Margin="4" Text="{Binding PrintMessage}" />
        <Button Content="Print" Width="80" Height="32" Command="{Binding PrintCommand}" />
    </StackPanel>
</Grid>

点击按钮,命令调用成功,

取消勾选, 按钮变灰,不可执行命令,

相关推荐
wangnaisheng10 小时前
【WPF】RenderTargetBitmap的使用
wpf
dotent·1 天前
WPF 完美解决改变指示灯的颜色
wpf
orangapple3 天前
WPF 用Vlc.DotNet.Wpf实现视频播放、停止、暂停功能
wpf·音视频
ysdysyn3 天前
wpf mvvm 数据绑定数据(按钮文字表头都可以),根据长度进行换行,并把换行的文字居中
c#·wpf·mvvm
orangapple3 天前
WPF 使用LibVLCSharp.WPF实现视频播放、停止、暂停功能
wpf
晚安苏州3 天前
WPF ControlTemplate 控件模板
wpf
晚安苏州3 天前
WPF 布局控件
wpf
吉量*3 天前
WPF系列二:窗口模式调整
wpf
吉量*3 天前
WPF系列一:窗口设置无边框
c#·.net·wpf·无边框