WPF:谈谈INotifyPropertyChanged中获取属性名字符串

一、介绍

在WPF中使用MVVM方式进行数据绑定时需要继承并实现INotifyPropertyChanged接口,当属性的值发生修改时触发PropertyChanged事件的执行,PropertyChanged执行需要传递PropertyChangedEventArgs类型参数,而PropertyChangedEventArgs参数需要一个字符串类型的参数,这个参数就是调用属性的名称。

属性名字符串的获取方式在不断演变,文章列出几种属性名获取的实现方式,希望在开发中对大家有所帮助。

    /// <summary>
    /// Provides data for the <see langword='PropertyChanged'/> event.
    /// </summary>
    public class PropertyChangedEventArgs : EventArgs
    {
        /// <summary>
        /// Initializes a new instance of the <see cref='System.ComponentModel.PropertyChangedEventArgs'/>
        /// class.
        /// </summary>
        public PropertyChangedEventArgs(string? propertyName)
        {
            PropertyName = propertyName;
        }

        /// <summary>
        /// Indicates the name of the property that changed.
        /// </summary>
        public virtual string? PropertyName { get; }
    }

二、第一种

    public class MainViewModel : System.ComponentModel.INotifyPropertyChanged
    {
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler? PropertyChanged;

        private void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion INotifyPropertyChanged
    }

这种实现方式是开始学习WPF MVVM时最多使用的,简单直接,属性名字符串用硬编码方式写进代码中,应用方式如下:

    public class MainViewModel : System.ComponentModel.INotifyPropertyChanged
    {
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler? PropertyChanged;

        private void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion INotifyPropertyChanged

        bool _isChecked1;
        public bool IsChecked1
        {
            get { return _isChecked1; }
            set { _isChecked1 = value; OnPropertyChanged("IsChecked1"); }
        }
    }

开发者需要在属性的Set方法中将属性名字符串传递到OnPropertyChanged方法,字符串的值必须要和属性名完全一致,多一个空格也会导致失败。这种写法的缺点是容易写错属性名字符串,并且需要修改属性名时还需要开发者手动修改对应的属性名字符串,因此这种实现方式一般不推荐在开发中使用。

三、第二种

    public class MainViewModel : System.ComponentModel.INotifyPropertyChanged
    {
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler? PropertyChanged;

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

        private void OnPropertyChanged(Expression<Func<MainViewModel, object>> propertyExpression)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(NameForProperty(propertyExpression)));
        }

        private string NameForProperty<TDelegate>(Expression<TDelegate> propertyExpression)
        {
            Expression body;
            var expression = propertyExpression.Body as UnaryExpression;
            if (expression != null)
                body = expression.Operand;
            else
                body = propertyExpression.Body;

            var member = body as MemberExpression;
            if (member == null)
                throw new ArgumentException("Property must be a MemberExpression");

            return member.Member.Name;
        }
        #endregion INotifyPropertyChanged
    }

第二种实现方式主要运用lamda表达式获取属性的属性名,解决第一种修改属性名麻锁的问题,可使用IDE的重构功能统一为该属性修改名字,并且通过IDE的智能提示一定程度上降低了输错属性名的可能性,但使用lamda表达式会带来额外的性能开销,不够第一种实现方式直接纯粹。应用方式如下:

        bool _isChecked2;
        public bool IsChecked2
        {
            get { return _isChecked2; }
            set { _isChecked2 = value; OnPropertyChanged(t=>t.IsChecked2); }
        }

四、第三种

第三种实现方式主要运用nameof表达式获取属性名,nameof表达式是C#6.0中新增的关键字,这个表达式可以根据变量来获取包含其名称的字符串,从而使开发人员不需要将变量名称写成字符串。

        bool _isChecked3;
        public bool IsChecked3
        {
            get { return _isChecked3; }
            set { _isChecked3 = value; OnPropertyChanged(nameof(IsChecked3)); }
        }

nameof表达式实际上是在编译阶段将属性名写成字符串,通过ILSpy反编译对比会发现最终的生成代码,这种方式没有了第二种实现方式额外的性能开销,但仍然会出现输错属性名的可能性,例如两个属性名非常接近。

 

 五、第四种

    public class MainViewModel : System.ComponentModel.INotifyPropertyChanged
    {
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler? PropertyChanged;

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

        private void OnPropertyChanged(Expression<Func<MainViewModel, object>> propertyExpression)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(NameForProperty(propertyExpression)));
        }

        private string NameForProperty<TDelegate>(Expression<TDelegate> propertyExpression)
        {
            Expression body;
            var expression = propertyExpression.Body as UnaryExpression;
            if (expression != null)
                body = expression.Operand;
            else
                body = propertyExpression.Body;

            var member = body as MemberExpression;
            if (member == null)
                throw new ArgumentException("Property must be a MemberExpression");

            return member.Member.Name;
        }

        #endregion INotifyPropertyChanged
    }

第四种方式是使用CallerMemberNameAttribute特征自动获取属性名字符串,CallerMemberNameAttribute是.Net Framework4.5中开始加入的特性类,作用是允许获取方法调用方的方法或属性名称。

        bool _isChecked4;
        public bool IsChecked4
        {
            get { return _isChecked4; }
            set { _isChecked4 = value; OnPropertyChanged(); }
        }

和nameof运行符一样CallerMemberNameAttribute已经在编译阶段生成相关的代码,不同点是不需要开发编写额外的代码。

至此第四种实现方式好像已经非常完美了,但是总结四种实现方式会发现都是统一的格式:定义一个变量然后把该变量经过clr属性包装,这样不可避免地需要开发人员编写很多枯燥重复的代码,因此下面介绍的第五种方法将非常优雅地解决此问题。

 六、第五种

第五种实现方法需要两个第3方类库:Fody和PropertyChanged.Fody,从nuget上将两个类库添加到工程中。

 在工程中添加(如果没有再添加)FodyWeavers.xsd和FodyWeavers.xml文件:

 

FodyWeavers.xsd

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
  <xs:element name="Weavers">
    <xs:complexType>
      <xs:all>
        <xs:element name="PropertyChanged" minOccurs="0" maxOccurs="1">
          <xs:complexType>
            <xs:attribute name="InjectOnPropertyNameChanged" type="xs:boolean">
              <xs:annotation>
                <xs:documentation>Used to control if the On_PropertyName_Changed feature is enabled.</xs:documentation>
              </xs:annotation>
            </xs:attribute>
            <xs:attribute name="TriggerDependentProperties" type="xs:boolean">
              <xs:annotation>
                <xs:documentation>Used to control if the Dependent properties feature is enabled.</xs:documentation>
              </xs:annotation>
            </xs:attribute>
            <xs:attribute name="EnableIsChangedProperty" type="xs:boolean">
              <xs:annotation>
                <xs:documentation>Used to control if the IsChanged property feature is enabled.</xs:documentation>
              </xs:annotation>
            </xs:attribute>
            <xs:attribute name="EventInvokerNames" type="xs:string">
              <xs:annotation>
                <xs:documentation>Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.</xs:documentation>
              </xs:annotation>
            </xs:attribute>
            <xs:attribute name="CheckForEquality" type="xs:boolean">
              <xs:annotation>
                <xs:documentation>Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.</xs:documentation>
              </xs:annotation>
            </xs:attribute>
            <xs:attribute name="CheckForEqualityUsingBaseEquals" type="xs:boolean">
              <xs:annotation>
                <xs:documentation>Used to control if equality checks should use the Equals method resolved from the base class.</xs:documentation>
              </xs:annotation>
            </xs:attribute>
            <xs:attribute name="UseStaticEqualsFromBase" type="xs:boolean">
              <xs:annotation>
                <xs:documentation>Used to control if equality checks should use the static Equals method resolved from the base class.</xs:documentation>
              </xs:annotation>
            </xs:attribute>
            <xs:attribute name="SuppressWarnings" type="xs:boolean">
              <xs:annotation>
                <xs:documentation>Used to turn off build warnings from this weaver.</xs:documentation>
              </xs:annotation>
            </xs:attribute>
            <xs:attribute name="SuppressOnPropertyNameChangedWarning" type="xs:boolean">
              <xs:annotation>
                <xs:documentation>Used to turn off build warnings about mismatched On_PropertyName_Changed methods.</xs:documentation>
              </xs:annotation>
            </xs:attribute>
          </xs:complexType>
        </xs:element>
      </xs:all>
      <xs:attribute name="VerifyAssembly" type="xs:boolean">
        <xs:annotation>
          <xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
        </xs:annotation>
      </xs:attribute>
      <xs:attribute name="VerifyIgnoreCodes" type="xs:string">
        <xs:annotation>
          <xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
        </xs:annotation>
      </xs:attribute>
      <xs:attribute name="GenerateXsd" type="xs:boolean">
        <xs:annotation>
          <xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
        </xs:annotation>
      </xs:attribute>
    </xs:complexType>
  </xs:element>
</xs:schema>

FodyWeavers.xml

<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
	<PropertyChanged/>
</Weavers>

将两个文件的属性改为 生成操作:无,复制到输出目录:不复制

代码中添加一个clr属性: 

public bool IsChecked5 { get; set; }

编译代码:

 从反编译可以看到,编译器自动添加了代码,并且这些代码没有带来额外的性能开销。

注:Fody和PropertyChanged.Fody两个类库相当于IDE的功能插件,指示IDE在编译时在适当的位置插入IL代码,并且程序运行时完全不需要这两个类库文件。

结论:推荐WPF开发中使用Fody和PropertyChanged.Fody,将一些枯燥重复格式的代码交给IDE帮我们自动完成。

MainWindow.xaml

<Window x:Class="Demo.INotifyPropertyChanged.MainWindow"
        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:local="clr-namespace:Demo.INotifyPropertyChanged"
        mc:Ignorable="d"
        WindowStartupLocation="CenterScreen"
        d:DataContext="{d:DesignInstance local:MainViewModel}"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel Orientation="Vertical" VerticalAlignment="Center" Margin="10">
            <StackPanel.Resources>
                <Style TargetType="StackPanel">
                    <Setter Property="Orientation" Value="Horizontal"/>
                    <Setter Property="Margin" Value="0,10"/>
                    <Setter Property="HorizontalAlignment" Value="Center"/>
                </Style>
            </StackPanel.Resources>
            <StackPanel Orientation="Horizontal">
                <CheckBox IsChecked="{Binding IsChecked1}" Content="{Binding IsChecked1}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <CheckBox IsChecked="{Binding IsChecked2}" Content="{Binding IsChecked2}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <CheckBox IsChecked="{Binding IsChecked3}" Content="{Binding IsChecked3}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <CheckBox IsChecked="{Binding IsChecked4}" Content="{Binding IsChecked4}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <CheckBox IsChecked="{Binding IsChecked5}" Content="{Binding IsChecked5}"/>
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

MainWindow.xaml.cs

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainViewModel();
        }
    }

MainViewModel.cs

namespace Demo.INotifyPropertyChanged
{
    public class MainViewModel : System.ComponentModel.INotifyPropertyChanged
    {
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler? PropertyChanged;

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

        private void OnPropertyChanged(Expression<Func<MainViewModel, object>> propertyExpression)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(NameForProperty(propertyExpression)));
        }

        private string NameForProperty<TDelegate>(Expression<TDelegate> propertyExpression)
        {
            Expression body;
            var expression = propertyExpression.Body as UnaryExpression;
            if (expression != null)
                body = expression.Operand;
            else
                body = propertyExpression.Body;

            var member = body as MemberExpression;
            if (member == null)
                throw new ArgumentException("Property must be a MemberExpression");

            return member.Member.Name;
        }

        #endregion INotifyPropertyChanged

        public bool IsChecked5 { get; set; }

        bool _isChecked4;
        public bool IsChecked4
        {
            get { return _isChecked4; }
            set { _isChecked4 = value; OnPropertyChanged(); }
        }

        bool _isChecked3;
        public bool IsChecked3
        {
            get { return _isChecked3; }
            set { _isChecked3 = value; OnPropertyChanged(nameof(IsChecked3)); }
        }

        bool _isChecked2;
        public bool IsChecked2
        {
            get { return _isChecked2; }
            set { _isChecked2 = value; OnPropertyChanged(t=>t.IsChecked2); }
        }

        bool _isChecked1;
        public bool IsChecked1
        {
            get { return _isChecked1; }
            set { _isChecked1 = value; OnPropertyChanged("IsChecked1"); }
        }


    }
}

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
WPF,可以使用DataTrigger来绑定ListBox每个Item的Index属性,并根据其值来设置Trigger的行为。 以下是一个示例,演示如何在Trigger获取ListBox每个Item的Index: ```xml <ListBox x:Name="MyListBox"> <ListBox.ItemContainerStyle> <Style TargetType="ListBoxItem"> <Setter Property="Background" Value="White"/> <Style.Triggers> <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=(ItemsControl.AlternationIndex)}" Value="0"> <Setter Property="Background" Value="LightBlue"/> </DataTrigger> <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=(ItemsControl.AlternationIndex)}" Value="1"> <Setter Property="Background" Value="LightGray"/> </DataTrigger> </Style.Triggers> </Style> </ListBox.ItemContainerStyle> <ListBoxItem Content="Item 1"/> <ListBoxItem Content="Item 2"/> <ListBoxItem Content="Item 3"/> <ListBoxItem Content="Item 4"/> <ListBoxItem Content="Item 5"/> </ListBox> ``` 在上面的示例,使用了ListBox的AlternationIndex属性获取每个Item的Index。 AlternationIndex是WPF的一个内置属性,它为ListBox的每个Item提供了一个唯一的整数值,可以用于在Trigger设置行为。 在上面的示例,如果Index为偶数,则将该Item的背景色设置为LightGray,如果为奇数,则将其背景色设置为LightBlue。 注意:在使用AlternationIndex属性时,需要将ListBox的AlternationCount属性设置为一个大于1的值,以便为每个Item分配一个唯一的Index。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值