数据绑定

数据绑定允许我们将UI控件的属性与数据源(例如对象、集合、数据库等)的属性绑定起来。当数据源发生变化时,UI控件会自动更新,反之亦然。
优点:将控件的UI同数据本身的逻辑分离,怎么显示是UI的事情,而显示的内容我负责给出来就行了,通过数据绑定可以避免在代码中引用UI的
WPF提供了四种主要的绑定模式:

绑定模式

  1. OneWay(单向绑定)

    • 数据从源(source)流向目标(target),但反过来不会更新。默认的绑定方式
    <TextBlock Text="{Binding Path=Name}" />
    

    等效于

    <TextBlock Text="{Binding Name}" />
    
  2. TwoWay(双向绑定)

    • 数据在源和目标之间双向流动,任何一方的变化都会更新另一方。
    <TextBox Text="{Binding Path=Name, Mode=TwoWay}" />
    
  3. OneWayToSource(单向到源)

    • 数据从目标流向源,但源的变化不会影响目标。
    <TextBox Text="{Binding Path=Name, Mode=OneWayToSource}" />
    
  4. OneTime(一次性绑定)

    • 仅在绑定时更新一次目标,之后源的变化不会再更新目标。
    <TextBlock Text="{Binding Path=Name, Mode=OneTime}" />
    

注意的是,如果使用的绑定模式是OneWay或者TwoWay,还需要属性更改通知机制,一般是实现INotifyPropertyChanged或者INotifyCollectionChanged接口

绑定源

数据绑定的源可以是各种对象,包括CLR对象、集合、XML数据等。

  1. CLR对象

    • 可以是任何普通的C#对象。(如果需要页面显示更新需要实现对应通知接口)
    public class Person
    {
        public string Name { get; set; }
    }
    
  2. 集合

    • 绑定到集合时,常使用ObservableCollection<T>,它会在集合变化时通知UI。相对应的前端控件是itemsControl
    public ObservableCollection<Person> People { get; set; }
    
  3. XML数据

    • 也可以绑定到XML数据源。
    <XmlDataProvider x:Key="xmlData" Source="data.xml" XPath="Books/Book"/>
    

属性更改通知

INotifyPropertyChanged
  • 用于通知属性值更改。
  public class Person : INotifyPropertyChanged
  {
      private string name;
      public string Name
      {
          get { return name; }
          set
          {
              name = value;
              OnPropertyChanged(nameof(Name));
          }
      }

      public event PropertyChangedEventHandler PropertyChanged;

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

注意我们并没有手动订阅 PropertyChanged事件,因为WPF会自动监听这个事件,事件被触发时更新与该属性绑定的 UI 元素。所以要注意目标类要是public的,不然WPF可能无法访问到这个对象

下面是更加通用的设置写法

internal class Person:INotifyPropertyChanged
{
    private string name;

    public string Name
    {
        get => name;
        set => SetField(ref name, value);
    }
    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}
INotifyCollectionChanged和ObservableCollection<T>

INotifyCollectionChanged是集合版本的属性更改通知,当集合发生变化的时候也会通知页面更新数据,而ObservableCollection<T>是前者的内置实现
注意ObservableCollection<T> 适用于通知集合本身的变化(如添加、删除项),但不包括项内部属性的变化。为了处理对象内部属性的变化,还需要确保这些对象实现 INotifyPropertyChanged 接口。

public partial class MainWindow : Window
{
    public ObservableCollection<Person> People { get; set; }

    public MainWindow()
    {
        InitializeComponent();

        People = new ObservableCollection<Person>
        {
            new Person { Name = "Alice" },
            new Person { Name = "Bob" },
            new Person { Name = "Charlie" }
        };

        this.DataContext = this;
    }
}
<Window x:Class="YourNamespace.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ListBox ItemsSource="{Binding People}" DisplayMemberPath="Name"/>
    </Grid>
</Window>

集合视图:

绑定到集合以后,对原有的数据进行排序,筛选或分组,这些操作不会影响基础数据源本身,一个数据源可以有多个不同的视图来通过不同方式显示数据
如果不指定视图直接绑定到集合,那么WPF会绑定到默认视图,默认视图会共享,所以更改了默认视图所有绑定默认视图的控件都会改变
可以通过GetDefaultView获取默认视图

  • CollectionView: 它是对数据集合的一个视图,用于支持排序、筛选、分组和导航。所有的集合视图都派生自 CollectionView 类。
  • ICollectionView: 一个接口,定义了集合视图的基本功能,如当前项、排序、筛选和分组等。

常用集合视图类

  • ListCollectionView: 适用于实现 IList 接口的集合,如 List<T>。最快的
  • BindingListCollectionView: 适用于实现 IBindingList 接口的集合,如 BindingList<T>
  • CollectionViewSource: 提供集合视图的 XAML 友好版本,可以用于资源字典和绑定。

指定视图
ListCollectionView:

  public ObservableCollection<Person> people { get; set; }
  public ListCollectionView PeopleView { get; set; }
  public MainWindow()
  {
      people = new ObservableCollection<Person>
      {
          new Person { Name = "Alice" },
          new Person { Name = "Bob" },
          new Person { Name = "Charlie" }
      };
      PeopleView = new ListCollectionView(people);
  
      InitializeComponent();


      this.DataContext = this;
  }
 <ListBox Width="200" Height="100" ItemsSource="{Binding PeopleView}" DisplayMemberPath="Name">
     <!--<ListBoxItem Content="Item 1" />
     <ListBoxItem Content="Item 2" />
     <ListBoxItem Content="Item 3" />-->
 </ListBox>

CollectionViewSource:

  <Window.Resources>

          <CollectionViewSource
              Source="{Binding Source={x:Static Application.Current}, Path=AuctionItems}"
              x:Key="ListingDataView" />
  </Window.Resources>
<ListBox Name="Master" 
         ItemsSource="{Binding Source={StaticResource ListingDataView}}">
    <ListBox.GroupStyle>
        <GroupStyle
            HeaderTemplate="{StaticResource GroupingHeaderTemplate}" />
    </ListBox.GroupStyle>
</ListBox>
 private readonly CollectionViewSource _listingDataView;

 public MainWindow()
 {
     InitializeComponent();
     _listingDataView = (CollectionViewSource) (Resources["ListingDataView"]);
 }

排序

可以通过 SortDescriptions 属性添加排序描述,排序可以按一个或多个属性进行。

ListCollectionView view = new ListCollectionView(data);
view.SortDescriptions.Add(new SortDescription("PropertyName", ListSortDirection.Ascending));

筛选

通过 Filter 委托实现筛选,过滤掉不符合条件的项。

view.Filter = item =>
{
    var dataItem = item as YourDataType;
    return dataItem != null && dataItem.PropertyName == "SomeValue";
};
private void RemoveFiltering(object sender, RoutedEventArgs args)
{
    listingDataView.Filter -= ShowOnlyBargainsFilter;
}

 private void AddFiltering(object sender, RoutedEventArgs args)
 { 
     listingDataView.Filter += ShowOnlyBargainsFilter;
   
 }

 private void ShowOnlyBargainsFilter(object sender, FilterEventArgs e)
 {
     var product = e.Item as AuctionItem;
     if (product != null)
     {
         // Filter out products with price 25 or above
         e.Accepted = product.CurrentPrice < 25;
     }
 }
 <CheckBox Name="Filtering" Grid.Row="1" Grid.Column="1"
           Checked="AddFiltering" Unchecked="RemoveFiltering"
           Margin="8" Style="{StaticResource CheckBoxStyle}">
     Show only bargains
 </CheckBox>

分组

使用 GroupDescriptions 属性进行分组。

view.GroupDescriptions.Add(new PropertyGroupDescription("CategoryProperty"));
     private void AddSorting(object sender, RoutedEventArgs args)
     {
         // This sorts the items first by Category and within each Category,
         // by StartDate. Notice that because Category is an enumeration,
         // the order of the items is the same as in the enumeration declaration
         listingDataView.SortDescriptions.Add(
             new SortDescription("Category", ListSortDirection.Ascending));
         listingDataView.SortDescriptions.Add(
             new SortDescription("StartDate", ListSortDirection.Ascending));
     }

     private void RemoveSorting(object sender, RoutedEventArgs args)
     {
         listingDataView.SortDescriptions.Clear();
     }

ICollectionView 提供了导航当前项的方法,如 MoveCurrentToNextMoveCurrentToPrevious 等。(当前项类似于集合里的遍历指针)

当数据集合较大时,使用排序、筛选和分组可能会导致性能问题,特别是在 UI 线程中执行时。

DependencyProperty

  • WPF控件使用依赖属性来支持高级功能(如数据绑定、动画等)。
public class MyControl : Control
{
    public static readonly DependencyProperty MyProperty =
        DependencyProperty.Register("My", typeof(string), typeof(MyControl));

    public string My
    {
        get { return (string)GetValue(MyProperty); }
        set { SetValue(MyProperty, value); }
    }
}

DataContext

DataContext 定义了绑定源的数据对象,从而使得 XAML 中的绑定可以访问到这个数据对象的属性DataContext 是你告诉 WPF "这里是数据的来源"。

绑定声明后,会在当前元素的数据上下文中解析绑定,如果没有找到,则往父节点的数据上下文查找,直到XAML的根节点

也支持指定特定对象进行解析而不从DataContext中查找

dataContext设置可以在XAML中,也可以在后台的C#代码里面

  <Window.DataContext>
      <local:Person Name="John Doe" />
  </Window.DataContext>

  <TextBlock Text="{Binding Name}" />

local那一行是声明并实例化一个 Person 对象,local 是在 XAML 文件顶部定义的命名空间前缀,指向包含 Person 类的命名空间。
层级关系将其将其设置为 Window 的数据上下文。

public MainWindow()
{
    InitializeComponent();
    this.DataContext = new Person { Name = "John Doe" };
    //this.DataContext = this;
}

对应的后台代码如上所示设置
也有直接将windows设成datacontext的,这样直接绑定到 Window 或 UserControl 实例的公共属性。

也可以专门设置一个ViewModel,存放数据

public class MainViewModel
{
    public string Title { get; set; }
    public UserViewModel User { get; set; }
    public ObservableCollection<ItemViewModel> Items { get; set; }

    public MainViewModel()
    {
        Title = "My Application";
        User = new UserViewModel { Name = "John Doe" };
        Items = new ObservableCollection<ItemViewModel>
        {
            new ItemViewModel { Name = "Item 1" },
            new ItemViewModel { Name = "Item 2" }
        };
    }
}

也支持viewModel的嵌套

<ListBox ItemsSource="{Binding ItemViewModelCollection.Items}">

如果不指定,直接写ItemsSource="{Binding}"则是直接绑定到dataContext对象上

将数据绑定到非dataContext

下面的例子假设myData的默认构造参数会将器ColorName设置成red

<DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
           xmlns:c="clr-namespace:SDKSample">
    <DockPanel.Resources>
        <c:MyData x:Key="myDataSource"/>
    </DockPanel.Resources>
    <Button Background="{Binding Source={StaticResource myDataSource}, Path=ColorName}"
            Width="150" Height="30">
        I am bound to be RED!
    </Button>
</DockPanel>
 <StackPanel>
     <!-- 第一个按钮,背景色为蓝色 -->
     <Button x:Name="Button1" Content="Button 1" Background="Blue" Width="100" Height="50" />

     <!-- 第二个按钮,背景色绑定到第一个按钮的背景色 -->
     <Button Content="Button 2" Width="100" Height="50"
             Background="{Binding Path=Background, ElementName=Button1}" />
 </StackPanel>

sourceElementName都可以用来指定绑定数据源
Source更通用,可以绑定到静态资源、对象、静态属性等,适用于数据源与 UI 控件没有直接关系的情况。
ElementName 用于在 XAML 控件之间进行绑定,适用于需要访问特定控件的属性的情况。

// Make a new source
var myDataObject = new MyData();
var myBinding = new Binding("ColorName")
{
    Source = myDataObject
};

// Bind the data source to the TextBox control's Text dependency property
myText.SetBinding(TextBlock.TextProperty, myBinding);
Binding.UpdateSourceTrigger

通过设置绑定的这个属性,可以设置什么时候更新源数据
如果 UpdateSourceTrigger 值为 UpdateSourceTrigger.PropertyChanged ,则目标属性更改后,TwoWayOneWayToSource 绑定源数据值会立即更新。 但是,如果 UpdateSourceTrigger 值为 LostFocus ,则仅当目标属性失去焦点时才会使用新值更新该值。

类型转换器

如果绑定到的目标属性不是想要的格式,需要实现 IValueConverter来创建自定义转换器让绑定生效
下面的例子将color转换为SolidColorBrush,后者是BackGround可以接受的值

[ValueConversion(typeof(Color), typeof(SolidColorBrush))]
public class ColorBrushConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        Color color = (Color)value;
        return new SolidColorBrush(color);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return null;
    }
}
public class BooleanToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (bool)value ? Visibility.Visible : Visibility.Collapsed;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
<Window.Resources>
    <local:BooleanToVisibilityConverter x:Key="BoolToVisConverter"/>
</Window.Resources>

<CheckBox IsChecked="{Binding IsVisible}" />
<TextBlock Text="Visible" Visibility="{Binding IsVisible, Converter={StaticResource BoolToVisConverter}}" />

数据模板

当直接绑定对象时,如果不指定模板,那么会显示集合里面对象的ToString方法,如果没有重写ToString,那么得到的会是无效的显示,可以通过指定DisplayMemberPath来简单指定想要的属性

<ListBox Width="200" Height="100" ItemsSource="{Binding People}" DisplayMemberPath="Name">

而通过使用数据模板,可以自定义集合的显示方式

<ListBox ItemsSource="{Binding People}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}" />
                <TextBlock Text="{Binding Age}" Margin="10,0,0,0"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

(绑定里面的,DataTemplate的DataContext会自动填充为people里面的元素)
(更多内容参考数据模板章节)

数据验证

数据验证(Validation)可以确保用户输入的数据满足应用程序的要求。验证规则可以通过实现ValidationRule类来定义,并与数据绑定(Data Binding)结合使用。

  1. 定义验证规则
    创建一个继承自ValidationRule的类,并重写其Validate方法。这个方法接收输入值并返回一个ValidationResult对象。
using System.Globalization;
using System.Windows.Controls;

public class NotEmptyValidationRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
        {
            return new ValidationResult(false, "Value cannot be empty.");
        }
        return ValidationResult.ValidResult;
    }
}
  1. 在XAML中定义绑定和验证规则

在XAML中设置BindingValidationRules属性将验证规则关联到控件。


    <Grid>
        <TextBox Name="inputTextBox" Width="200" Height="30" VerticalAlignment="Center" HorizontalAlignment="Center">
            <TextBox.Text>
                <Binding Path="InputText" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True">
                    <Binding.ValidationRules>
                        <local:NotEmptyValidationRule />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
    </Grid>
  1. 定义数据模型和数据绑定
using System.ComponentModel;

public class ViewModel : INotifyPropertyChanged
{
    private string _inputText;

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

    public event PropertyChangedEventHandler PropertyChanged;

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

在窗口的构造函数中设置数据上下文:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new ViewModel();
    }
}
  1. 提供视觉反馈

可以使用Validation.ErrorTemplate属性来定义控件在验证失败时的外观。例如,显示红色边框和错误信息。

<Window.Resources>
    <ControlTemplate x:Key="validationTemplate">
        <DockPanel>
            <TextBlock Foreground="Red" Margin="5,0,0,0" DockPanel.Dock="Right" Text="{Binding [0].ErrorContent}" />
            <AdornedElementPlaceholder/>
        </DockPanel>
    </ControlTemplate>
</Window.Resources>

<Grid>
    <TextBox Name="inputTextBox" Width="200" Height="30" VerticalAlignment="Center" HorizontalAlignment="Center">
        <TextBox.Text>
            <Binding Path="InputText" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True">
                <Binding.ValidationRules>
                    <local:NotEmptyValidationRule />
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
        <TextBox.Style>
            <Style TargetType="TextBox">
                <Style.Triggers>
                    <Trigger Property="Validation.HasError" Value="true">
                        <Setter Property="ToolTip"
                                Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
                        <Setter Property="BorderBrush" Value="Red"/>
                        <Setter Property="BorderThickness" Value="2"/>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </TextBox.Style>
    </TextBox>
</Grid>

内置类:

WPF中提供了两个内置的ValidationRule类:ExceptionValidationRuleDataErrorValidationRule。它们分别用于处理异常和数据错误。

  1. ExceptionValidationRule
    ExceptionValidationRule用于处理在数据绑定过程中抛出的异常。它会捕获绑定过程中发生的任何异常,并将其视为验证错误。
<TextBox Width="200" Height="30">
    <TextBox.Text>
        <Binding Path="Age" UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <ExceptionValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>
public class ViewModel : INotifyPropertyChanged
{
    private int _age;
    
    public int Age
    {
        get { return _age; }
        set
        {
            if (_age != value)
            {
                if (value < 0)
                {
                    throw new ArgumentException("Age cannot be negative");
                }
                _age = value;
                OnPropertyChanged(nameof(Age));
            }
        }
    }

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

2. DataErrorValidationRule

DataErrorValidationRule用于处理实现了IDataErrorInfo接口的对象的验证。它会检查绑定目标对象是否实现了IDataErrorInfo接口,并使用该接口提供的错误信息进行验证。

<TextBox Width="200" Height="30">
    <TextBox.Text>
        <Binding Path="Name" UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <DataErrorValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>
public class ViewModel : INotifyPropertyChanged, IDataErrorInfo
{
    private string _name;
    private int _age;

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

    public int Age
    {
        get { return _age; }
        set
        {
            if (_age != value)
            {
                _age = value;
                OnPropertyChanged(nameof(Age));
            }
        }
    }

    public string Error => null;

    public string this[string columnName]
    {
        get
        {
            string result = null;
            if (columnName == nameof(Name))
            {
                if (string.IsNullOrWhiteSpace(Name))
                {
                    result = "Name cannot be empty.";
                }
            }
            else if (columnName == nameof(Age))
            {
                if (Age < 0 || Age > 120)
                {
                    result = "Age must be between 0 and 120.";
                }
            }
            return result;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
<Window.Resources>
    <ControlTemplate x:Key="validationTemplate">
        <DockPanel>
            <TextBlock Foreground="Red" Margin="5,0,0,0" DockPanel.Dock="Right" Text="{Binding [0].ErrorContent}" />
            <AdornedElementPlaceholder/>
        </DockPanel>
    </ControlTemplate>
</Window.Resources>

<Grid>
    <TextBox Name="inputTextBox" Width="200" Height="30" VerticalAlignment="Center" HorizontalAlignment="Center">
        <TextBox.Text>
            <Binding Path="Age" UpdateSourceTrigger="PropertyChanged">
                <Binding.ValidationRules>
                    <ExceptionValidationRule />
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
        <TextBox.Style>
            <Style TargetType="TextBox">
                <Style.Triggers>
                    <Trigger Property="Validation.HasError" Value="true">
                        <Setter Property="ToolTip"
                                Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
                        <Setter Property="BorderBrush" Value="Red"/>
                        <Setter Property="BorderThickness" Value="2"/>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </TextBox.Style>
    </TextBox>
</Grid>

绑定到枚举
没有直接的方法将枚举用作数据绑定源,但是Enum.GetValues(Type)方法返回枚举的集合,可以用来作为数据源

<Window.Resources>
    <ObjectDataProvider x:Key="EnumDataSource"
                        ObjectType="{x:Type sys:Enum}"
                        MethodName="GetValues">
        <ObjectDataProvider.MethodParameters>
            <x:Type TypeName="HorizontalAlignment" />
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
</Window.Resources>

ObjectDataProvider里面的代码等效于

var enumDataSource = System.Enum.GetValues(typeof(System.Windows.HorizontalAlignment));

使用:

<ListBox Name="myComboBox" SelectedIndex="0"
         ItemsSource="{Binding Source={StaticResource EnumDataSource}}"/>
综合使用:

假设你有一个按钮,你希望它的启用/禁用状态和颜色由后台(ViewModel)控制。

Step 1: 定义数据模型

首先,定义一个Person类,包含一个属性IsEnabled用于控制按钮的启用/禁用状态。

using System.ComponentModel;

public class Person : INotifyPropertyChanged
{
    private string name;
    private bool isEnabled;

    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            OnPropertyChanged(nameof(Name));
        }
    }

    public bool IsEnabled
    {
        get { return isEnabled; }
        set
        {
            isEnabled = value;
            OnPropertyChanged(nameof(IsEnabled));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

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

Step 2: 定义ViewModel

然后,定义一个PeopleViewModel类,该类包含一个Person对象,并实现属性更改通知。

using System.Collections.ObjectModel;
using System.ComponentModel;

public class PeopleViewModel : INotifyPropertyChanged
{
    private Person selectedPerson;

    public Person SelectedPerson
    {
        get { return selectedPerson; }
        set
        {
            selectedPerson = value;
            OnPropertyChanged(nameof(SelectedPerson));
        }
    }

    public ObservableCollection<Person> People { get; set; }

    public PeopleViewModel()
    {
        People = new ObservableCollection<Person>
        {
            new Person { Name = "John Doe", IsEnabled = true },
            new Person { Name = "Jane Smith", IsEnabled = false },
        };

        SelectedPerson = People[0];
    }

    public event PropertyChangedEventHandler PropertyChanged;

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

Step 3: 绑定到XAML

在XAML中,将DataContext设置为PeopleViewModel的实例,并绑定按钮的IsEnabled属性和背景颜色属性。

<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="350" Width="525">
    <Window.DataContext>
        <local:PeopleViewModel />
    </Window.DataContext>
    <Grid>
        <ListBox ItemsSource="{Binding People}" SelectedItem="{Binding SelectedPerson}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button Content="Action"
                IsEnabled="{Binding SelectedPerson.IsEnabled}"
                Background="{Binding SelectedPerson.IsEnabled, Converter={StaticResource BoolToBrushConverter}}"
                HorizontalAlignment="Center"
                VerticalAlignment="Bottom"
                Width="100"
                Height="50" />
    </Grid>
</Window>

Step 4: 值转换器

定义一个IValueConverter来根据IsEnabled属性转换按钮的背景颜色。

using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;

public class BoolToBrushConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool isEnabled)
        {
            return isEnabled ? Brushes.LightGreen : Brushes.LightGray;
        }
        return Brushes.Transparent;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

在XAML中定义资源:

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

Step 5: 更新后台代码

在代码后面(代码隐藏文件MainWindow.xaml.cs中),可以根据某些条件来更改SelectedPersonIsEnabled属性,并观察UI的变化。

using System.Windows;

namespace WpfApp
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void ToggleButton_Click(object sender, RoutedEventArgs e)
        {
            var viewModel = (PeopleViewModel)DataContext;
            if (viewModel.SelectedPerson != null)
            {
                viewModel.SelectedPerson.IsEnabled = !viewModel.SelectedPerson.IsEnabled;
            }
        }
    }
}

在XAML中添加一个切换按钮以测试:

<Button Content="Toggle Enable"
        Click="ToggleButton_Click"
        HorizontalAlignment="Center"
        VerticalAlignment="Bottom"
        Margin="0,10,0,0" />

当你运行应用程序时,ListBox中的项会显示Person的名称。当选择不同的Person时,按钮的启用/禁用状态和背景颜色会根据SelectedPerson.IsEnabled属性进行更新。点击“Toggle Enable”按钮可以切换SelectedPerson.IsEnabled属性,并实时更新UI。

绑定的属性
Path

指定绑定目标的属性路径。
Path=User.Name 绑定到 User 对象的 Name 属性。

Source

设置绑定的数据源对象。如果指定了 Source,则 Binding 会直接从这个对象获取数据。
Source={StaticResource MyData}`

ElementName

绑定到 XAML 中指定名称的元素的属性。
ElementName=myTextBox, Path=Text

RelativeSource

指定定义绑定源的位置相对于当前元素的相对位置。这使得可以从当前元素的上下文中绑定数据,而不需要显式地指定源对象的路径。

<TextBlock Text="{Binding Path=DataContext.Title, RelativeSource={RelativeSource AncestorType=Window}}"/>

绑定到Windows的DataContext 的Title
绑定到自己

<TextBlock Text="{Binding Path=ActualWidth, RelativeSource={RelativeSource Self}}"/>

绑定到当前父元素

<Border Background="{Binding Path=Background, RelativeSource={RelativeSource TemplatedParent}}"/>
Mode

指定绑定的操作模式,决定数据如何在源和目标之间流动。
- OneWay:源到目标的单向绑定,目标不可修改。
- TwoWay:双向绑定,源和目标都可以更新。
- OneWayToSource:目标到源的单向绑定。
- OneTime:绑定只发生一次,适合初始化。

UpdateSourceTrigger
指定何时更新绑定源的值。

 - `Default`:使用默认触发方式(通常为控件的事件)。
 - `PropertyChanged`:每当目标属性更改时更新源。
 - `LostFocus`:当控件失去焦点时更新源。
 - `Explicit`:仅在调用 `UpdateSource` 时更新。

Converter
用于将绑定的值转换为适合目标的值,或将目标值转换为适合源的值。
Converter={StaticResource MyValueConverter}

ConverterParameter
传递给 Converter 的额外参数。
ConverterParameter="SomeParameter"`

ConverterCulture
指定用于转换的文化信息。
ConverterCulture="en-US"

FallbackValue
在绑定失败时使用的值。
FallbackValue="N/A"

TargetNullValue
绑定源值为 null 时使用的值。
TargetNullValue="No Value"

StringFormat
用于格式化绑定值的字符串格式。
StringFormat="{}{0:C}"(格式化为货币)

BindingGroup
给绑定分组,对一组绑定进行整体验证和更新。例如可以在提交前,对所有表单里面的数据一起验证
BindingGroup="{Binding MyBindingGroup}"

UpdateSourceExceptionFilter
一个方法,用于处理绑定源更新时发生的异常。
UpdateSourceExceptionFilter="HandleException"`

ValidatesOnDataErrors
指定是否启用 IDataErrorInfo 验证。
ValidatesOnDataErrors=True

ValidatesOnExceptions
指定是否启用异常验证(INotifyDataErrorInfo)。
ValidatesOnExceptions=True

NotifyOnSourceUpdated
指定是否在源更新时通知绑定目标。默认为Flase,改为True会触发Binding.SourceUpdated事件
NotifyOnSourceUpdated=True

NotifyOnTargetUpdated
指定是否在目标更新时通知绑定源。默认为Flase,改为True会触发Binding.TargetUpdated事件
NotifyOnTargetUpdated=True

NotifyOnValidationError
指定是否在验证失败时通知绑定。
NotifyOnValidationError=True

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容

  • WPF之Binding介绍 一、什么是数据绑定 请参考微软官方文档数据绑定概述 - WPF .NET | Micr...
    Elvis523阅读 3,188评论 0 1
  • 数据绑定 教程数据 NoesisGUI提供了一种简单而强大的方法来自动更新业务模型和用户界面之间的数据。这种机制称...
    YottaYuan阅读 666评论 0 1
  • 自定义Binding 当为Binding设置了继承System.ComponentModel.INotifyPro...
    李霖弢阅读 1,456评论 0 0
  • 概念 数据绑定连接两个对象,称为源和目标。源对象提供数据。目标对象将消耗(并经常显示)来自源对象的数据。例如,La...
    Lulus阅读 261评论 0 0
  • 注:本文出现的所有代码为了简单明了均省略了很多细节部分,只注重原理,直接复制粘贴运行得不到对应的结果。 WPF的数...
    quchangTJU阅读 4,251评论 0 4