以类型安全的方式处理 PropertyChanged

发布于 2024-09-18 09:52:04 字数 448 浏览 7 评论 0原文

有很多文章介绍如何使用反射和 LINQ 以类型安全的方式引发 PropertyChanged 事件,而不使用字符串。

但是有没有办法以类型安全的方式使用 PropertyChanged 事件呢?目前,我正在这样做

void model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    switch (e.PropertyName)
    {
        case "Property1":
            ...
        case "Property2":
            ...

        ....               
    }
}

有没有办法避免在 switch 语句中硬编码字符串来处理不同的属性?一些类似的基于 LINQ 或基于反射的方法?

There have been plenty of articles about how to use reflection and LINQ to raise PropertyChanged events in a type-safe way, without using strings.

But is there any way to consume PropertyChanged events in a type-safe manner? Currently, I'm doing this

void model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    switch (e.PropertyName)
    {
        case "Property1":
            ...
        case "Property2":
            ...

        ....               
    }
}

Is there any way to avoid hard-coding strings in a switch statement to handle the different properties? Some similar LINQ- or reflection-based approach?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

吃颗糖壮壮胆 2024-09-25 09:52:04

对于 C# 6.0,您可以使用 nameof。您还可以引用类的属性,而无需创建该类的实例。

void model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    switch (e.PropertyName)
    {
        case nameof(ClassName.Property1):
            ...
        case nameof(ClassName.Property2):
            ...

        ....               
    }
}

With C# 6.0 you can use nameof. You can also reference a class' property without creating an instance of that class.

void model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    switch (e.PropertyName)
    {
        case nameof(ClassName.Property1):
            ...
        case nameof(ClassName.Property2):
            ...

        ....               
    }
}
大姐,你呐 2024-09-25 09:52:04

让我们声明一个可以将 lambda 表达式转换为 Reflection PropertyInfo 对象的方法 (取自我的回答):

public static PropertyInfo GetProperty<T>(Expression<Func<T>> expr)
{
    var member = expr.Body as MemberExpression;
    if (member == null)
        throw new InvalidOperationException("Expression is not a member access expression.");
    var property = member.Member as PropertyInfo;
    if (property == null)
        throw new InvalidOperationException("Member in expression is not a property.");
    return property;
}

然后让我们用它来获取属性的名称:

void model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == GetProperty(() => Property1).Name)
    {
        // ...
    }
    else if (e.PropertyName == GetProperty(() => Property2).Name)
    {
        // ...
    }
}

不幸的是你不能使用 switch 语句,因为属性名称不再是编译时常量。

Let’s declare a method that can turn a lambda expression into a Reflection PropertyInfo object (taken from my answer here):

public static PropertyInfo GetProperty<T>(Expression<Func<T>> expr)
{
    var member = expr.Body as MemberExpression;
    if (member == null)
        throw new InvalidOperationException("Expression is not a member access expression.");
    var property = member.Member as PropertyInfo;
    if (property == null)
        throw new InvalidOperationException("Member in expression is not a property.");
    return property;
}

And then let’s use it to get the names of the properties:

void model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == GetProperty(() => Property1).Name)
    {
        // ...
    }
    else if (e.PropertyName == GetProperty(() => Property2).Name)
    {
        // ...
    }
}

Unfortunately you can’t use a switch statement because the property names are no longer compile-time constants.

梦在深巷 2024-09-25 09:52:04

我最近提出的一个解决方案是将事件调度逻辑封装到一个专用的类中。

该类有一个名为 Handle 的公共方法,它与 PropertyChangedEventHandler 委托具有相同的签名,这意味着它可以订阅任何类的 PropertyChanged 事件实现 INotifyPropertyChanged 接口。

该类接受委托,就像大多数 WPF 实现中经常使用的 DelegateCommand 一样,这意味着无需创建子类即可使用它。

该类如下所示:

public class PropertyChangedHandler
{
    private readonly Action<string> handler;
    private readonly Predicate<string> condition;
    private readonly IEnumerable<string> properties;

    public PropertyChangedHandler(Action<string> handler, 
        Predicate<string> condition, IEnumerable<string> properties)
    {
        this.handler = handler;
        this.condition = condition;
        this.properties = properties;
    }

    public void Handle(object sender, PropertyChangedEventArgs e)
    {
        string property = e.PropertyName ?? string.Empty;

        if (this.Observes(property) && this.ShouldHandle(property))
        {
            handler(property);
        }
    }

    private bool ShouldHandle(string property)
    {
        return condition == null ? true : condition(property);
    }

    private bool Observes(string property)
    {
        return string.IsNullOrEmpty(property) ? true :
            !properties.Any() ? true : properties.Contains(property);
    }
}

然后,您可以像这样注册属性更改事件处理程序:

var eventHandler = new PropertyChangedHandler(
    handler: p => { /* event handler logic... */ },
    condition: p => { /* determine if handler is invoked... */ },
    properties: new string[] { "Foo", "Bar" }
);

aViewModel.PropertyChanged += eventHandler.Handle;

PropertyChangedHandler 负责检查 PropertyChangedEventArgsPropertyName并确保通过正确的属性更改来调用handler

请注意,PropertyChangedHandler 还接受谓词,以便可以有条件地分派处理程序委托。该类还允许您指定多个属性,以便单个处理程序可以一次性绑定到多个属性。

这可以使用一些扩展方法轻松扩展,以实现更方便的处理程序注册,这允许您创建事件处理程序并在单个方法调用中订阅 PropertyChanged 事件,并使用表达式而不是字符串指定属性实现看起来像这样的东西:

aViewModel.OnPropertyChanged(
    handler: p => handlerMethod(),
    condition: p => handlerCondition,
    properties: aViewModel.GetProperties(
        p => p.Foo,
        p => p.Bar,
        p => p.Baz
    )
);

这基本上是说,当 FooBarBaz 属性更改 handlerMethodhandlerCondition 为 true,则将调用 >。

提供了 OnPropertychanged 方法的重载来满足不同的事件注册要求。

例如,如果您想要注册一个为任何属性更改事件调用并且始终执行的处理程序,您可以简单地执行以下操作:

aViewModel.OnPropertyChanged(p => handlerMethod());

例如,如果您想要注册一个始终执行但仅针对单个事件的处理程序具体属性更改您可以执行以下操作:

aViewModel.OnPropertyChanged(
    handler: p => handlerMethod(),
    properties: aViewModel.GetProperties(p => p.Foo)
);

我发现这种方法在编写 WPF MVVM 应用程序时非常有用。想象一下,您有一个场景,当三个属性中的任何一个发生更改时,您希望使命令无效。使用普通方法,您必须执行以下操作:

void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
{
    switch (e.PropertyName)
    {
        case "Foo":
        case "Bar":
        case "Baz":
            FooBarBazCommand.Invalidate();
            break;
        ....               
    }
}

如果更改任何 viewModel 属性的名称,您将需要记住更新事件处理程序以选择正确的属性。

使用上面指定的 PropertyChangedHandler 类,您可以通过以下方式获得相同的结果:

aViewModel.OnPropertyChanged(
    handler: p => FooBarBazCommand.Invalidate(),
    properties: aViewModel.GetProperties(
        p => p.Foo,
        p => p.Bar,
        p => p.Baz
    )
);

现在它具有编译时安全性,因此如果重命名任何 viewModel 属性,程序将无法编译。

A recent solution I have come up with is to encapsulate the event dispatch logic into a dedicated class.

The class has a public method called Handle which has the same signature as the PropertyChangedEventHandler delegate meaning it can be subscribed to the PropertyChanged event of any class that implements the INotifyPropertyChanged interface.

The class accepts delegates like the often used DelegateCommand used by most WPF implementations meaning it can be used without having to create subclasses.

The class looks like this:

public class PropertyChangedHandler
{
    private readonly Action<string> handler;
    private readonly Predicate<string> condition;
    private readonly IEnumerable<string> properties;

    public PropertyChangedHandler(Action<string> handler, 
        Predicate<string> condition, IEnumerable<string> properties)
    {
        this.handler = handler;
        this.condition = condition;
        this.properties = properties;
    }

    public void Handle(object sender, PropertyChangedEventArgs e)
    {
        string property = e.PropertyName ?? string.Empty;

        if (this.Observes(property) && this.ShouldHandle(property))
        {
            handler(property);
        }
    }

    private bool ShouldHandle(string property)
    {
        return condition == null ? true : condition(property);
    }

    private bool Observes(string property)
    {
        return string.IsNullOrEmpty(property) ? true :
            !properties.Any() ? true : properties.Contains(property);
    }
}

You can then register a property changed event handler like this:

var eventHandler = new PropertyChangedHandler(
    handler: p => { /* event handler logic... */ },
    condition: p => { /* determine if handler is invoked... */ },
    properties: new string[] { "Foo", "Bar" }
);

aViewModel.PropertyChanged += eventHandler.Handle;

The PropertyChangedHandler takes care of checking the PropertyName of the PropertyChangedEventArgs and ensures that handler is invoked by the right property changes.

Notice that the PropertyChangedHandler also accepts a predicate so that the handler delegate can be conditionally dispatched. The class also allows you to specify multiple properties so that a single handler can be bound to multiple properties in one go.

This can easily be extended using some extensions methods for more convenient handler registration which allows you to create the event handler and subscribe to the PropertyChanged event in a single method call and specify the properties using expressions instead of strings to achieve something that looks like this:

aViewModel.OnPropertyChanged(
    handler: p => handlerMethod(),
    condition: p => handlerCondition,
    properties: aViewModel.GetProperties(
        p => p.Foo,
        p => p.Bar,
        p => p.Baz
    )
);

This is basically saying that when either the Foo, Bar or Baz properties change handlerMethod will be invoked if handlerCondition is true.

Overloads of the OnPropertychanged method are provided to cover different event registration requirements.

If, for example, you want to register a handler that is called for any property changed event and is always executed you can simply do the following:

aViewModel.OnPropertyChanged(p => handlerMethod());

If, for example, you want to register a handler that is always executed but only for a single specific property change you can do the following:

aViewModel.OnPropertyChanged(
    handler: p => handlerMethod(),
    properties: aViewModel.GetProperties(p => p.Foo)
);

I have found this approach very useful when writing WPF MVVM applications. Imagine you have a scenario where you want to invalidate a command when any of three properties change. Using the normal method you would have to do something like this:

void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
{
    switch (e.PropertyName)
    {
        case "Foo":
        case "Bar":
        case "Baz":
            FooBarBazCommand.Invalidate();
            break;
        ....               
    }
}

If you change the name of any of the viewModel properties you will need to remember to update the event handler to select the correct properties.

Using the PropertyChangedHandler class specified above you can achieve the same result with the following:

aViewModel.OnPropertyChanged(
    handler: p => FooBarBazCommand.Invalidate(),
    properties: aViewModel.GetProperties(
        p => p.Foo,
        p => p.Bar,
        p => p.Baz
    )
);

This now has compile-time safety so If any of the viewModel properties are renamed the program will fail to compile.

私野 2024-09-25 09:52:04

Josh Smith 的 MVVM Foundation 包含一个 PropertyObserver 类,可以执行您想要的操作。

Josh Smith's MVVM Foundation includes a PropertyObserver class that does what you want.

吖咩 2024-09-25 09:52:04

我通过组合命令模式和一些表达式逻辑来避免这种切换。您将案例操作封装在命令中。
我将使用模型视图控制器结构来说明这一点。 想法是相同的

现实世界代码 - WinForms,但当在模型中设置 Tree 属性时,示例在视图中加载树的

自定义 ICommand

void Execute();
string PropertyName  { get;  }

具体命令

 public TreeChangedCommand(TreeModel model, ISelectTreeView selectTreeView,Expression<Func<object>> propertyExpression )
    {
        _model = model;
        _selectTreeView = selectTreeView;

        var body = propertyExpression.Body as MemberExpression;
        _propertyName = body.Member.Name;

    }

构造函数控制器

 //handle notify changed event from model
  _model.PropertyChanged += _model_PropertyChanged;
  //init commands
  commands = new List<ICommand>();
  commands.Add(new TreeChangedCommand(_model,_mainView,()=>_model.Tree));

propertyChanged 处理程序

void _model_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    //find the corresponding command and execute it. (instead of the switch)
    commands.FirstOrDefault(cmd=>cmd.PropertyName.Equals(e.PropertyName)).Execute();
}

I avoid the switch by combining the command pattern and some Expression logic. You encapsulate the case-action in a command.
I'll illustrate this using a Model View Controller structure. real world code - WinForms,but it is the same idea

the example loads a tree in a view, when the Tree property is set in the model.

a custom ICommand

void Execute();
string PropertyName  { get;  }

Concrete Command

 public TreeChangedCommand(TreeModel model, ISelectTreeView selectTreeView,Expression<Func<object>> propertyExpression )
    {
        _model = model;
        _selectTreeView = selectTreeView;

        var body = propertyExpression.Body as MemberExpression;
        _propertyName = body.Member.Name;

    }

constructor controller

 //handle notify changed event from model
  _model.PropertyChanged += _model_PropertyChanged;
  //init commands
  commands = new List<ICommand>();
  commands.Add(new TreeChangedCommand(_model,_mainView,()=>_model.Tree));

propertyChanged handler

void _model_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    //find the corresponding command and execute it. (instead of the switch)
    commands.FirstOrDefault(cmd=>cmd.PropertyName.Equals(e.PropertyName)).Execute();
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文