如何在WPF MVVM视图模型中消除重复属性

8

我正在设置一个WPF应用程序,其中包含许多属性的ViewModel。这些属性都非常重复,我想知道是否有方法可以摆脱它们。以下是一个属性的示例,我大约有8-10个这样的属性。

public string Name
{
    get
    {
        return this.name;
    }

    set
    {
        if (this.name != value)
        {
            this.name = value;
            this.RaisePropertyChanged("Name");
        }
    }
}

1
可能是实现INotifyPropertyChanged - 是否存在更好的方法?的重复问题。 - McGarnagle
谢谢,很有意思的阅读。 - uncletall
确实...我想复杂性在于决定“相等”意味着什么。对我来说,我认为我宁愿使用冗长的setter(以及一个VS代码片段来减轻反复编写的痛苦)。 - McGarnagle
在回答这个问题时(我已经放弃了),我发现了一个NuGet包:UpdateControls:http://updatecontrols.net/cs/index.shtml 从网页上来看,“Update Controls不需要您实现INotifyPropertyChanged或声明DependencyProperty。它直接将控件连接到CLR属性。”我会自己调查这个包。希望这对你有所帮助。此外,我在这里找到了UpdateControls的信息:http://blog.excastle.com/2012/12/16/stop-writing-inotifypropertychanged-start-using-updatecontrols/ - philologon
4个回答

5

如果您的需求比较简单,我的建议是选择第三方解决方案。幸运的是,由于一些聪明人的努力,这个问题已经被解决了...

您可以采用最简单的方式编写代码,完全删除INotifyPropertyChanged实现,并以最小化的方式编写属性,如下所示:

public string Name { get; set; }

然后将Fody.PropertyChanged添加到项目中(它在NuGet上),并在类上打上[ImplementPropertyChanged]属性。
Fody会在编译期间进行一些聪明的IL魔法,自动实现接口和所有样板代码,这意味着你编写的代码非常简洁,最终结果也是你想要的。
请注意,如果你的代码中还依赖于INotifyPropertyChanged接口(即,如果你在代码中手动附加事件或类似操作),你可能需要以不同的方式使用Fody,因为IDE不会意识到你已经实现了该接口。幸运的是,Fody也会在其他情况下自动实现(例如:在一个类中实现INotifyPropertyChanged,Fody默认也会实现属性变化时的事件触发)。

谢谢Dan,这看起来也很有趣。似乎有很多方法可以解决这个问题,仅仅看一个简单的东西就可以让我忙上几天...你知道Fody在Visual Studio之外运行构建时是否也有效吗? - uncletall
Fody 当然也适用于服务器和命令行构建,如果这是你的意思的话。(我相信它会将一个后置构建步骤注入到项目文件中,以允许它注入处理程序,但我没有任何可用的代码来确认这一点。) - Dan Puzey

2
这段话的翻译是:“提到的线程确实包含了答案,但你需要进行一些挖掘。我将展示我在其中找到的两个最佳答案。”
“第一个解决方案是实现一个ViewModelBase类,该类将set方法封装到模板方法中,并使用lambda表达式来检索属性名称,以便重构不会破坏属性名称字符串。”
public class ViewModelBase: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

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

    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
    {
        if (selectorExpression == null)
            throw new ArgumentNullException("selectorExpression");
        var body = selectorExpression.Body as MemberExpression;
        if (body == null)
            throw new ArgumentException("The body must be a member expression");
        OnPropertyChanged(body.Member.Name);
    }

    protected bool SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(selectorExpression);
        return true;
    }
}

使用方法:

class ViewModel : DataBase
{
    private String _prop1;
    public String Prop1
    {
        get { return _prop1; }
        set
        {
            SetField(ref _prop1, value, () => Prop1);
        }
    }
}

第二种解决方案使用字典来存储基类中的属性。这样,我们不需要传递旧值,因为它保存在基类中,也不需要创建成员字段来保存属性的值。我最喜欢这种解决方案。
public abstract class ViewModelBase : INotifyPropertyChanged
{
    private readonly Dictionary<string, object> _propertyValueStorage;

    #region Constructor

    protected ViewModelBase()
    {
        this._propertyValueStorage = new Dictionary<string, object>();
    }

    #endregion

    protected void SetValue<T>(Expression<Func<T>> property, T value)
    {
        var lambdaExpression = property as LambdaExpression;

        if (lambdaExpression == null)
        {
            throw new ArgumentException("Invalid lambda expression", "Lambda expression return value can't be null");
        }

        var propertyName = this.getPropertyName(lambdaExpression);
        var storedValue = this.getValue<T>(propertyName);

        if (object.Equals(storedValue, value)) return;

        this._propertyValueStorage[propertyName] = value;
        this.OnPropertyChanged(propertyName);
    }

    protected T GetValue<T>(Expression<Func<T>> property)
    {
        var lambdaExpression = property as LambdaExpression;

        if (lambdaExpression == null)
        {
            throw new ArgumentException("Invalid lambda expression", "Lambda expression return value can't be null");
        }

        var propertyName = this.getPropertyName(lambdaExpression);
        return getValue<T>(propertyName);
    }

    private T getValue<T>(string propertyName)
    {
        object value;
        if (_propertyValueStorage.TryGetValue(propertyName, out value))
        {
            return (T)value;
        }
        return default(T);

    }

    private string getPropertyName(LambdaExpression lambdaExpression)
    {
        MemberExpression memberExpression;

        if (lambdaExpression.Body is UnaryExpression)
        {
            var unaryExpression = lambdaExpression.Body as UnaryExpression;
            memberExpression = unaryExpression.Operand as MemberExpression;
        }
        else
        {
            memberExpression = lambdaExpression.Body as MemberExpression;
        }

        return memberExpression.Member.Name;
    }

    #region < INotifyPropertyChanged > Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion

}

使用方法为:
public class ViewModel : ViewModelBase
{
    public String Prop1
    {
        get { return GetValue(() => Prop1); }
        set { SetValue(() => Prop1, value); }
    }
    public bool Bool1
    {
        get { return GetValue(() => Bool1); }
        set { SetValue(() => Bool1, value); }
    }

方案1基于https://dev59.com/bHM_5IYBdhLWcg3wlEPO#1316566https://dev59.com/bHM_5IYBdhLWcg3wlEPO#1316566 方案2基于http://dotnet-forum.de/blogs/thearchitect/archive/2012/11/01/die-optimale-implementierung-des-inotifypropertychanged-interfaces.aspx

值得注意的是,您可以使用.NET4.5 CallerMemberName 属性,而不是从表达式中解析属性名称。 - Dan Puzey
你说得没错,但我还在使用4.0版本。不过仅仅为了这个,升级到VS2012/13也值得推进。 - uncletall
如果您引用Microsoft.BCL.Portability并安装KB2468871,就可以在.net4项目中使用CallMemeberName。 - Lonli-Lokli
谢谢,我会查看KB2468871。 - uncletall
似乎有点复杂,需要让每个人都能够构建服务器来使用它。 - uncletall
使用第二个。虽然拥有对象列表并且仅将属性用作键可能会感觉有些不好,但您可以避免重复和大量的样板文件。 - Magus

0

我的解决方案与uncletall的接近,但有一些用法上的变化。

    private static readonly Properties<MainWindowViewModel> _properties = new Properties<MainWindowViewModel>();

    public static Property TextProperty = _properties.Create(_ => _.Text);
    private string _text;
    public string Text
    {
        get { return _text; }
        set { SetProperty(ref _text, value, TextProperty); }
    }

XAML:

<Label Grid.Row="1" Content="{Model:PropertyBinding  {x:Static Model:MainWindowViewModel.TextProperty}}" Width="200"/>

这个示例的好处是可以在编译时检查更改。 完整示例 link


感谢提供这个工作示例项目。但是我不完全理解您提到的编译时检查的优点,您的解决方案相比我的有什么优势吗?但劣势在于需要额外添加两个语句来创建属性和定义字段。 - uncletall
我认为XAML有很大的好处——任何属性名称的更改都会导致XAML的更改,而不会出现运行时错误。是的,定义_props是必需的。但无论如何,代码始终保持可读和易懂。 - Lonli-Lokli
此外,如果您想要实现一个“魔法”,那么您需要实现自定义解决方案。这并不容易。我们正在生产代码中使用我们的自定义属性通知器,但我不能与您分享信息。您可以将所有可能的易于实现的方法作为答案来支持自动通知。 - Lonli-Lokli

0

这取决于需求,如果所有的属性都被用于相同的目的,比如name1、name2、name3...name10,像列出10个人的名字,那么就把它们放在另一个类中,并将该类的集合类型绑定到你的xaml文件中的Items-control。或者简单地绑定一个ObservableCollection的字符串

但是,如果每个属性都有自己的目的,那么就无法避免这种情况,因为属性只是用来保存不同值的变量。每个属性都会有自己的意图,在视图模型中对每个属性的操作也会因逻辑而异。


我正在使用它作为实时应用程序,在不同区域显示状态信息。 - uncletall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接