具有自动属性名称实现的 MVVM INotifyPropertyChanged

Posted

技术标签:

【中文标题】具有自动属性名称实现的 MVVM INotifyPropertyChanged【英文标题】:MVVM INotifyPropertyChanged with Automatic Property Name implementation 【发布时间】:2013-02-11 20:25:55 【问题描述】:

据我了解,我们可以在 MVVM 风格的应用程序中使用 INofityProperty,代码类似于以下

    object _SelectedPerson;
    public object SelectedPerson
    
        get
        
            return _SelectedPerson;
        
        set
        
            if (_SelectedPerson != value)
            
                _SelectedPerson = value;
                RaisePropertyChanged("SelectedPerson");
            
        
    

现在,我看到Josh Smith's excellent example 他实现了额外的代码来捕获如果开发人员键入无法识别的属性名称(例如拼写错误)会发生什么!

如果你讨厌这个,请告诉我,但是有一种方法可以从堆栈跟踪中获取方法名称。所以,我们可以改为实现类似

    object _SelectedPerson;
    public object SelectedPerson
    
        get
        
            return _SelectedPerson;
        
        set
        
            if (_SelectedPerson != value)
            
                _SelectedPerson = value;
                RaisePropertyChanged(Current.Method);
            
        
    

static class Current

    public static string Method()
    
        StackTrace st = new StackTrace();
        return (st.GetFrame(1).GetMethod().Name.Split('_')[1]);            
    

我只能假设这将始终有效,因为 RaisePropertyChanged 事件总是发生在 Setter 中(如果我错了,请纠正我)。

现在请注意,我无法真正尝试这个,因为在工作中(我可以从事更大的项目)我仍在使用 .NET 2.0,因此 WPF/MVVM 还很遥远在未来,但我在自己的时间学习。

所以,我的问题是来自那些使用过它的人,与删除错误选项相比,有一种方法来提醒用户错误真的更好吗(或者你觉得我错过了什么? );问题是,乔什·史密斯是公认的,是该领域的专家,所以如果他建议这种方法,那么通常我会盲目地遵循,但在这种情况下,我不禁要问它,觉得有必要了解更多。

【问题讨论】:

【参考方案1】:

我已经对管理 propertychanged 通知进行了自己的实现调整。

第一部分是经典的 NotifierBase 类:

/// <summary>
///   Base class for all notifying objects (model adapters, view models, etc.)
/// </summary>
public abstract class NotifierBase : INotifyPropertyChanged

    /// <summary>
    ///   Private reference to UI thread
    /// </summary>
    private readonly System.Windows.Threading.Dispatcher _uiThread;

    /// <summary>
    ///   Default Constructor
    /// </summary>
    protected NotifierBase()
    
        _uiThread = Application.Current != null ? Application.Current.Dispatcher : System.Windows.Threading.Dispatcher.CurrentDispatcher;
    

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    #endregion

    /// <summary>
    ///   Explicit raise of a property changed notification
    /// </summary>
    /// <param name="e"> </param>
    protected void Notify(PropertyChangedEventArgs e)
    
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        
            //Debug method used to verify that the property we are about to raise is really a member of the current class instance
            CheckProperty(e.PropertyName);

            //raises the notification
            ToUiThread(() => handler(this, e));
        
    

    protected void Notify(params PropertyChangedEventArgs[] e)
    
        foreach (var pcea in e)
        
            Notify(pcea);
        
    

    /// <summary>
    ///   Dispatch an action to the ui thread
    /// </summary>
    /// <param name="action"> Action to dispatch </param>
    protected void ToUiThread(Action action)
    
        if (_uiThread.CheckAccess()) //if we are already in the UI thread, invoke action
            action();
        else
        
            //otherwise dispatch in the ui thread
            _uiThread.Invoke(action);
        
    

    /// <summary>
    ///   Check if the raised property is a valid property for the current instance type
    /// </summary>
    /// <param name="propertyName"> Name of the raised property </param>
    [DebuggerStepThrough]
    private void CheckProperty(string propertyName)
    
        Type type = GetType();
        PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
        if (properties.Any(pi => pi.Name == propertyName)) return;

        throw new InvalidOperationException(
            string.Format("Trying to raise notification on property \"0\" which does not exists on type \"1\"",
                          propertyName, type.Name));
    

基本上,这个类提供: - 一个简单的 UI 线程调度功能 - 对通知的属性进行调试检查 - 多属性通知功能

第二部分是集中类,它重新组合了我的应用程序中使用的所有通知事件参数。如果一个属性被多次使用,这主要是避免在整个应用程序中使用多个相同的硬编码字符串。但总体而言,代码重构和可维护性也更容易。

public class Npcea 


    public static readonly PropertyChangedEventArgs BarCode = new PropertyChangedEventArgs("BarCode");
    public static readonly PropertyChangedEventArgs Cap = new PropertyChangedEventArgs("Cap");
    public static readonly PropertyChangedEventArgs Code = new PropertyChangedEventArgs("Code");
    public static readonly PropertyChangedEventArgs Status = new PropertyChangedEventArgs("Status");
    public static readonly PropertyChangedEventArgs Comments = new PropertyChangedEventArgs("Comments");

所以基本上,在您的视图模型(或适配器或其他)中,您只需通知类似的属性

    public ResourceStatus Status
    
        get  return _status; 
        set
        
            _status = value;
            Notify(Npcea.Status,Npcea.Comments);
        
    

希望这会有所帮助

--布鲁诺

【讨论】:

【参考方案2】:

使用 StackTrace 的问题是,它没有在发布版本中正确填充。为了克服这个问题,有几种方法可以修复它并使开发人员更容易引发 PropertyChanged 事件。

查看这个问题:Implementing INotifyPropertyChanged - does a better way exist? 并选择适合您的解决方案 :)

我个人更喜欢以下:

// Helper method
public static PropertyChangedEventArgs CreateArguments<TOwner>(Expression<Func<TOwner, object>> Expression) 
    // determine the Name of the property using the Expression


// Within the view-model implementations:
private static readonly PropertyChangedEventArgs TitleProperty = CreateArguments<MyViewModel>(m => m.Title);

private string title;

public string Title 
    get  return this.title; 
    set 
        if (!string.Equals(this.title, value) 
            this.title = value;
            this.OnPropertyChanged(TitleProperty);
        
    

通过使用静态成员预生成 PropertyChangedEventArgs,检查表达式树引入的开销是有限的。此解决方案是重构安全的,因此您没有任何魔术字符串。

我也喜欢使用 CallerMemberNameAttribute 的 .NET 4.5 方法,但它似乎不适用于可移植类库。

【讨论】:

这很好,但我没有收到您关于“未在发布版本中正确填充”的评论。你有什么我可以读到的(甚至是我可以自己谷歌的短语吗?)+1 我的意思是你不能信任发布版本中的 StackTrace,因为代码可能会被优化并且方法可能是内联的。如果是属性,你可能会很好,但我一般会避免使用 StackTrace。关于这个主题有几个 SO 讨论,例如:***.com/questions/7585010/…【参考方案3】:

您可以通过抽象基类执行 INotifyPropertyChanged。这可能如下所示:

        public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Event, fired when the Property has changed
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="propertyExpression">() => this.Param</param>
    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
    
        var propertyName = ExtractPropertyName(propertyExpression);
        OnPropertyChanged(propertyName);
    

    protected void OnPropertyChanged(string propertyName)
    
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        
            handler(this, new PropertyChangedEventArgs(propertyName));
        
    

    /// <summary>
    /// Extracts the propertyname out of the Expression given
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="propertyExpression"></param>
    /// <returns></returns>
    private static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
    
        var memberExpression = propertyExpression.Body as MemberExpression;
        return memberExpression == null ? null : memberExpression.Member.Name;
    

在 .Net 4.5 中,您可以创建如下类:

 public class ViewModelBase : INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    

你只需要打电话

OnPropertyChanged();

【讨论】:

以上是关于具有自动属性名称实现的 MVVM INotifyPropertyChanged的主要内容,如果未能解决你的问题,请参考以下文章

searchkick - 具有多个属性的自动完成

使用 MVVM 创建具有现有外部属性的 EF Core 实体

MVVM和MVC的总结

如何正确绑定到 MVVM 框架中用户控件的依赖属性

mvvm双向绑定机制的原理和代码实现

js实现一个简单的MVVM框架