Triển khai INotifyPropertyChanged - có cách nào tốt hơn không?


647

Microsoft nên đã triển khai một cái gì đó linh hoạt INotifyPropertyChanged, như trong các thuộc tính tự động, chỉ cần xác định {get; set; notify;} tôi nghĩ rằng nó có ý nghĩa rất lớn để làm điều đó. Hoặc có bất kỳ biến chứng để làm điều đó?

Bản thân chúng ta có thể thực hiện một cái gì đó như 'thông báo' trong tài sản của mình không. Có một giải pháp duyên dáng để thực hiện INotifyPropertyChangedtrong lớp của bạn hay cách duy nhất để làm điều đó là bằng cách nâng cao PropertyChangedsự kiện trong mỗi tài sản.

Nếu không chúng ta có thể viết một cái gì đó để tự động tạo ra đoạn mã để nâng cao PropertyChanged sự kiện không?


7
code.google.com/p/notifypropertyweaver có thể được sử dụng
Ian Ringrose

7
liên kết trên là chết. github.com/SimonCropp/NotifyPropertyWeaver
Prime23

2
Thay vào đó, bạn có thể sử dụng DependencyObject và DependencyProperIES. HẠ! Tôi đã làm một điều buồn cười.
Phil


5
Tại thời điểm thực hiện các thay đổi đối với C # là không thể, chúng tôi đã có một bản ghi lại rất lớn các phụ thuộc lẫn nhau. Vì vậy, trở lại khi MVVM ra đời, tôi đoán rằng, chúng tôi thực sự đã không nỗ lực nhiều để giải quyết vấn đề này và tôi biết nhóm Mô hình & Thực tiễn đã có một vài bước đi (do đó bạn cũng có MEF như một phần của điều đó đề tài nghiên cứu). Hôm nay tôi nghĩ rằng [CallerMemberName] là câu trả lời cho câu hỏi trên.
Scott Barnes

Câu trả lời:


633

Không sử dụng cái gì đó như postsharp, phiên bản tối thiểu tôi sử dụng sử dụng cái gì đó như:

public class Data : INotifyPropertyChanged
{
    // boiler-plate
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
    protected bool SetField<T>(ref T field, T value, string propertyName)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    // props
    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }
}

Mỗi tài sản sau đó chỉ là một cái gì đó như:

    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }

cái không lớn lắm; nó cũng có thể được sử dụng như một lớp cơ sở nếu bạn muốn. Trả boolvề từ SetFieldcho bạn biết nếu đó là một no-op, trong trường hợp bạn muốn áp dụng logic khác.


hoặc thậm chí dễ dàng hơn với C # 5:

protected bool SetField<T>(ref T field, T value,
    [CallerMemberName] string propertyName = null)
{...}

có thể được gọi như thế này:

set { SetField(ref name, value); }

với trình biên dịch sẽ thêm "Name"tự động.


C # 6.0 giúp việc thực hiện dễ dàng hơn:

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

... và bây giờ với C # 7:

protected void OnPropertyChanged(string propertyName)
   => 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;
}

private string name;
public string Name
{
    get => name;
    set => SetField(ref name, value);
}

4
Marc lừa đẹp! Tôi đã đề xuất một cải tiến để sử dụng biểu thức lambda thay vì tên thuộc tính, xem câu trả lời của tôi
Thomas Levesque

7
@Thomas - lambda là tất cả tốt và tốt, nhưng nó thêm rất nhiều chi phí cho một cái gì đó thực sự rất đơn giản. Một mẹo hữu ích, nhưng tôi không chắc nó luôn thực tế.
Marc Gravell

14
@Marc - Có, nó có thể làm giảm hiệu suất ... Tuy nhiên tôi thực sự thích thực tế là nó đã được kiểm tra vào thời gian biên dịch và được tái cấu trúc chính xác bằng lệnh "Đổi tên"
Thomas Levesque

4
@Gusdor may mắn thay, với C # 5, không cần phải thỏa hiệp - bạn có thể tận dụng tốt nhất cả hai thông qua (như Pedro77 ghi chú)[CallerMemberName]
Marc Gravell

4
@Gusdor ngôn ngữ và khung là riêng biệt; bạn có thể sử dụng trình biên dịch C # 5, .NET 4 đích và chỉ cần thêm thuộc tính bị thiếu - nó sẽ hoạt động tốt. Nó chỉ cần có tên chính xác và nằm trong không gian tên chính xác. Nó không cần phải ở trong một hội đồng cụ thể.
Marc Gravell

196

Kể từ .Net 4.5 cuối cùng cũng có một cách dễ dàng để làm điều này.

.Net 4.5 giới thiệu một thuộc tính thông tin người gọi mới.

private void OnPropertyChanged<T>([CallerMemberName]string caller = null) {
     // make sure only to call this if the value actually changes

     var handler = PropertyChanged;
     if (handler != null) {
        handler(this, new PropertyChangedEventArgs(caller));
     }
}

Có lẽ cũng là một ý tưởng tốt để thêm một bộ so sánh cho chức năng.

EqualityComparer<T>.Default.Equals

Thêm ví dụ ở đâyđây

Đồng thời xem Thông tin người gọi (C # và Visual Basic)


12
Xuất sắc! Nhưng tại sao nó lại chung chung?
abatishchev

@abatishchev Tôi đoán là không cần thiết, tôi chỉ chơi với ý tưởng có chức năng thiết lập thuộc tính là tốt. Tôi sẽ xem nếu tôi có thể cập nhật câu trả lời của mình cung cấp giải pháp đầy đủ. Các ví dụ thêm làm một công việc tốt trong thời gian đó.
Daniel Little

3
Nó được giới thiệu bởi C # 5.0. Nó không có gì để làm với .net 4.5, nhưng đây là một giải pháp tuyệt vời!
J. Lennon

5
@J. Lennon .net 4.5 vẫn có một cái gì đó để làm với nó, sau khi tất cả các thuộc tính đến từ đâu đó msdn.microsoft.com/en-au/l Library / trộm
Daniel Little

@Lavinski thay đổi ứng dụng của bạn thành .NET 3.5 và xem những gì sẽ hoạt động (trong vs2012)
J. Lennon

162

Tôi thực sự thích giải pháp của Marc, nhưng tôi nghĩ rằng nó có thể được cải thiện một chút để tránh sử dụng "chuỗi ma thuật" (không hỗ trợ tái cấu trúc). Thay vì sử dụng tên thuộc tính làm chuỗi, thật dễ dàng để biến nó thành biểu thức lambda:

private string name;
public string Name
{
    get { return name; }
    set { SetField(ref name, value, () => Name); }
}

Chỉ cần thêm các phương thức sau vào mã của Marc, nó sẽ thực hiện thủ thuật:

protected virtual void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
{
    if (selectorExpression == null)
        throw new ArgumentNullException("selectorExpression");
    MemberExpression 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;
}

BTW, điều này được lấy cảm hứng từ bài đăng blog này được cập nhật URL


6
Có ít nhất một khung sử dụng phương pháp này, ReactiveUI .
AlSki

Rất muộn, điều này có nghĩa là trải qua sự phản ánh, có nghĩa là một hiệu suất hit. Có thể chấp nhận được, nhưng thiết lập một tài sản không phải là nơi mà tôi muốn ứng dụng của mình dành cho nhiều chu kỳ.
Bruno Brant

1
@BrunoBrant Bạn có chắc chắn có một hit hiệu suất? Theo bài đăng trên blog, sự phản chiếu xảy ra trong thời gian biên dịch thay vì thời gian chạy (tức là phản xạ tĩnh).
Nathaniel Elkins

6
Tôi tin rằng toàn bộ OnPropertyChanged <T> của bạn đã lỗi thời với toán tử tên của C # 6, làm cho con quái vật này trở nên bóng bẩy hơn một chút.
Traubenfuchs

5
@Traubenfuchs, trên thực tế, thuộc tính CallerMemberName của C # 5 làm cho nó thậm chí còn đơn giản hơn, vì bạn không cần phải vượt qua bất cứ điều gì cả ...
Thomas Levesque

120

Ngoài ra còn có Fody có bổ trợ PropertyChanged , cho phép bạn viết phần này:

[ImplementPropertyChanged]
public class Person 
{        
    public string GivenNames { get; set; }
    public string FamilyName { get; set; }
}

... và tại thời điểm biên dịch các thuộc tính thay đổi thông báo.


7
Tôi nghĩ rằng đây chính xác là những gì OP đang tìm kiếm khi họ hỏi "Chúng ta có thể tự mình thực hiện một cái gì đó như 'thông báo' trong tài sản của mình không. Có giải pháp nào duyên dáng để triển khai INotifyPropertyChanged trong lớp của bạn không"
Ashoat

3
Đây thực sự là giải pháp duyên dáng và nó hoạt động hoàn hảo như @CADbloke đã nói. Và tôi cũng hoài nghi về thợ dệt, nhưng tôi đã kiểm tra / kiểm tra lại mã IL phía sau và nó hoàn hảo, nó đơn giản, là tất cả những gì bạn cần và không ai khác. Nó cũng móc và gọi bất kỳ tên phương thức nào bạn đã chỉ định trong lớp cơ sở cho nó, cho dù NotifyOnProp ..., OnNotify ... không thành vấn đề, vì vậy hoạt động tốt với bất kỳ lớp cơ sở nào mà bạn có thể có và thực hiện INotify .. .
NSGaga hầu hết không hoạt động vào

1
Bạn có thể dễ dàng kiểm tra lại những gì thợ dệt đang làm, hãy nhìn vào cửa sổ đầu ra của bản dựng, nó liệt kê tất cả những thứ thuộc tính mà nó đã dệt. Sử dụng tiện ích mở rộng VScolorOutput với mẫu regex "Fody/.*?:",LogCustom2,Truelàm nổi bật nó trong màu "Tùy chỉnh 2". Tôi làm cho nó màu hồng sáng để dễ tìm. Chỉ cần Fody tất cả mọi thứ, đó là cách gọn gàng nhất để làm bất cứ điều gì có nhiều kiểu gõ lặp đi lặp lại.
CAD bloke

@mahmoudnezarsarhan không, không, tôi nhớ có một chút thay đổi trong cách cấu hình, nhưng Fody PropertyChanged vẫn còn sống và hoạt động.
Larry

65

Tôi nghĩ mọi người nên chú ý hơn một chút đến hiệu suất; nó thực sự ảnh hưởng đến giao diện người dùng khi có rất nhiều đối tượng bị ràng buộc (nghĩ về lưới có hơn 10.000 hàng) hoặc nếu giá trị của đối tượng thay đổi thường xuyên (ứng dụng theo dõi thời gian thực).

Tôi đã thực hiện các triển khai khác nhau được tìm thấy ở đây và ở nơi khác và làm một so sánh; kiểm tra xem so sánh hiệu suất của các triển khai INotifyPropertyChanged .


Đây là một cái nhìn vào kết quả Thực hiện so với thời gian chạy


14
-1: không có chi phí hoạt động: CallerMemberName được thay đổi thành giá trị bằng chữ tại thời gian biên dịch. Chỉ cần thử và dịch ngược ứng dụng của bạn.
JYL

đây là câu hỏi và câu trả lời theo: stackoverflow.com/questions/22580623/ từ
uli78

1
@JYL, bạn đúng là CallerMemberName không thêm chi phí lớn. Tôi phải thực hiện một cái gì đó sai thời gian qua tôi đã thử nó. Tôi sẽ cập nhật blog và trả lời để phản ánh điểm chuẩn cho việc triển khai CallerMemberName và Fody sau này.
Peijen

1
Nếu bạn có lưới 10.000+ trong UI thì có lẽ bạn nên kết hợp các cách tiếp cận để xử lý hiệu suất, như phân trang trong đó bạn chỉ hiển thị 10, 50, 100, 250 lượt truy cập mỗi trang ...
Austin Rhymer

Austin Rhymer, nếu bạn có dữ liệu lớn + 50 sử dụng ảo hóa dữ liệu thì không cần tải tất cả dữ liệu, nó sẽ chỉ tải dữ liệu hiển thị trên khu vực được hiển thị hiện tại!
Song phương

38

Tôi giới thiệu một lớp Bindable trong blog của mình tại http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/ Bindable sử dụng từ điển làm túi tài sản. Thật dễ dàng để thêm các quá tải cần thiết cho một lớp con để quản lý trường sao lưu của chính nó bằng các tham số ref.

  • Không có chuỗi ma thuật
  • Không có phản xạ
  • Có thể được cải thiện để chặn tra cứu từ điển mặc định

Mật mã:

public class Bindable : INotifyPropertyChanged {
    private Dictionary<string, object> _properties = new Dictionary<string, object>();

    /// <summary>
    /// Gets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    protected T Get<T>([CallerMemberName] string name = null) {
        Debug.Assert(name != null, "name != null");
        object value = null;
        if (_properties.TryGetValue(name, out value))
            return value == null ? default(T) : (T)value;
        return default(T);
    }

    /// <summary>
    /// Sets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <param name="name"></param>
    /// <remarks>Use this overload when implicitly naming the property</remarks>
    protected void Set<T>(T value, [CallerMemberName] string name = null) {
        Debug.Assert(name != null, "name != null");
        if (Equals(value, Get<T>(name)))
            return;
        _properties[name] = value;
        OnPropertyChanged(name);
    }

    public event PropertyChangedEventHandler PropertyChanged;

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

Nó có thể được sử dụng như thế này:

public class Contact : Bindable {
    public string FirstName {
        get { return Get<string>(); }
        set { Set(value); }
    }
}

2
Đây là một giải pháp tốt, nhưng nhược điểm duy nhất là có một màn trình diễn nhỏ liên quan đến quyền anh / unboxing.
MC gia

1
Tôi sẽ đề nghị sử dụng protected T Get<T>(T defaultValue, [CallerMemberName] string name = null)và cũng kiểm tra if (_properties.ContainsKey(name) && Equals(value, Get<T>(default(T), name)))trong Set (để tăng và lưu khi lần đầu tiên được đặt thành giá trị mặc định)
Miquel

1
@Miquel thêm hỗ trợ cho các giá trị mặc định tùy chỉnh có thể hữu ích chắc chắn, tuy nhiên bạn nên cẩn thận chỉ nâng sự kiện đã thay đổi khi giá trị thực sự thay đổi. Đặt thuộc tính cho cùng một giá trị mà nó không nên tăng sự kiện. Tôi phải thừa nhận trong hầu hết các trường hợp, điều đó vô hại, tuy nhiên tôi đã đôi lần gặp phải một số lần với các thuộc tính được đặt hàng nghìn lần cho cùng một giá trị với các sự kiện phá hủy phản ứng của UI.
TiMoch

1
@stakx Tôi có một vài ứng dụng dựa trên điều này để hỗ trợ mẫu memento cho hoàn tác / làm lại hoặc để kích hoạt đơn vị mẫu công việc trong các ứng dụng mà nhibernate không thể sử dụng được
TiMoch

1
Tôi thực sự thích giải pháp cụ thể này: ký hiệu ngắn, không có công cụ proxy động, không can thiệp IL, v.v. Mặc dù vậy, bạn có thể làm cho nó ngắn hơn bằng cách loại bỏ nhu cầu chỉ định T mỗi lần cho Get bằng cách làm cho Get return động. Tôi biết, điều này ảnh hưởng đến hiệu suất thời gian chạy, nhưng bây giờ mã cho getters và setters cuối cùng có thể luôn giống nhau và trong một dòng , ca ngợi Chúa! PS, bạn nên cẩn thận hơn trong phương thức Get của mình (một lần khi bạn viết lớp cơ sở) khi trả về các giá trị mặc định cho các giá trị là động. Hãy chắc chắn luôn trả về các giá trị mặc định chính xác (có thể được thực hiện)
evilkos

15

Tôi thực sự chưa có cơ hội để thử bản thân mình, nhưng lần tới, tôi đang thiết lập một dự án với yêu cầu lớn đối với INotifyPropertyChanged Tôi dự định viết một thuộc tính Postsharp sẽ tiêm mã vào thời gian biên dịch. Cái gì đó như:

[NotifiesChange]
public string FirstName { get; set; }

Sẽ trở thành:

private string _firstName;

public string FirstName
{
   get { return _firstname; }
   set
   {
      if (_firstname != value)
      {
          _firstname = value;
          OnPropertyChanged("FirstName")
      }
   }
}

Tôi không chắc liệu điều này có hiệu quả trong thực tế hay không và tôi cần phải ngồi xuống và thử nó, nhưng tôi không hiểu tại sao không. Tôi có thể cần làm cho nó chấp nhận một số tham số cho các tình huống có nhiều hơn một OnPropertyChanged cần được kích hoạt (ví dụ: nếu tôi có một thuộc tính FullName trong lớp ở trên)

Hiện tại tôi đang sử dụng một mẫu tùy chỉnh trong Resharper, nhưng ngay cả khi tôi đã chán ngấy tất cả các thuộc tính của mình quá lâu.


À, một tìm kiếm nhanh trên Google (mà tôi nên làm trước khi tôi viết bài này) cho thấy ít nhất một người đã làm một việc như thế này trước đây . Không chính xác những gì tôi đã nghĩ, nhưng đủ gần để cho thấy rằng lý thuyết là tốt.


6
Một công cụ miễn phí có tên Fody dường như làm điều tương tự, hoạt động như một trình tiêm mã thời gian biên dịch chung. Nó có thể tải xuống trong Nuget, cũng như các gói plugin PropertyChanged và PropertyChanging.
Triynko

11

Vâng, cách tốt hơn chắc chắn tồn tại. Đây là:

Hướng dẫn từng bước thu nhỏ của tôi, dựa trên bài viết hữu ích này .

  • Tạo dự án mới
  • Cài đặt gói lõi lâu đài vào dự án

Castle-Gói cài đặt.

  • Chỉ cài đặt thư viện ánh sáng mvvm

Cài đặt MvvmLightLibs

  • Thêm hai lớp trong dự án:

Trình thông báo

public class NotifierInterceptor : IInterceptor
    {
        private PropertyChangedEventHandler handler;
        public static Dictionary<String, PropertyChangedEventArgs> _cache =
          new Dictionary<string, PropertyChangedEventArgs>();

        public void Intercept(IInvocation invocation)
        {
            switch (invocation.Method.Name)
            {
                case "add_PropertyChanged":
                    handler = (PropertyChangedEventHandler)
                              Delegate.Combine(handler, (Delegate)invocation.Arguments[0]);
                    invocation.ReturnValue = handler;
                    break;
                case "remove_PropertyChanged":
                    handler = (PropertyChangedEventHandler)
                              Delegate.Remove(handler, (Delegate)invocation.Arguments[0]);
                    invocation.ReturnValue = handler;
                    break;
                default:
                    if (invocation.Method.Name.StartsWith("set_"))
                    {
                        invocation.Proceed();
                        if (handler != null)
                        {
                            var arg = retrievePropertyChangedArg(invocation.Method.Name);
                            handler(invocation.Proxy, arg);
                        }
                    }
                    else invocation.Proceed();
                    break;
            }
        }

        private static PropertyChangedEventArgs retrievePropertyChangedArg(String methodName)
        {
            PropertyChangedEventArgs arg = null;
            _cache.TryGetValue(methodName, out arg);
            if (arg == null)
            {
                arg = new PropertyChangedEventArgs(methodName.Substring(4));
                _cache.Add(methodName, arg);
            }
            return arg;
        }
    }

ProxyCreator

public class ProxyCreator
{
    public static T MakeINotifyPropertyChanged<T>() where T : class, new()
    {
        var proxyGen = new ProxyGenerator();
        var proxy = proxyGen.CreateClassProxy(
          typeof(T),
          new[] { typeof(INotifyPropertyChanged) },
          ProxyGenerationOptions.Default,
          new NotifierInterceptor()
          );
        return proxy as T;
    }
}
  • Tạo mô hình xem của bạn, ví dụ:

-

 public class MainViewModel
    {
        public virtual string MainTextBox { get; set; }

        public RelayCommand TestActionCommand
        {
            get { return new RelayCommand(TestAction); }
        }

        public void TestAction()
        {
            Trace.WriteLine(MainTextBox);
        }
    }
  • Đặt các ràng buộc vào xaml:

    <TextBox Text="{Binding MainTextBox}" ></TextBox>
    <Button Command="{Binding TestActionCommand}" >Test</Button>
  • Đặt dòng mã vào tệp phía sau mã MainWindow.xaml.cs như thế này:

DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();

  • Thưởng thức.

nhập mô tả hình ảnh ở đây

Chú ý!!! Tất cả các thuộc tính giới hạn phải được trang trí bằng từ khóa ảo vì chúng được sử dụng bởi proxy lâu đài để ghi đè.


Tôi muốn biết phiên bản Castle nào bạn đang sử dụng. Tôi đang sử dụng 3.3.0 và phương pháp CreateClassProxy không có những thông số: type, interfaces to apply, interceptors.
I sát thương

Nevermind, tôi đã sử dụng CreateClassProxy<T>phương pháp chung . Khác nhau nhiều ... hmmm, tự hỏi tại sao lại hạn chế với phương pháp chung chung như vậy. :(
Tôi chấp nhận


5

Xem tại đây: http://dotnet-forum.de/bloss/thearchitect/archive/2012/11/01/die-optimale-im THỰCierung-des-inifypropertychanged-interfaces.aspx

Nó được viết bằng tiếng Đức, nhưng bạn có thể tải xuống ViewModelBase.cs. Tất cả các ý kiến ​​trong cs-File được viết bằng tiếng Anh.

Với ViewModelBase-Class này, có thể triển khai các thuộc tính ràng buộc tương tự như Thuộc tính phụ thuộc nổi tiếng:

public string SomeProperty
{
    get { return GetValue( () => SomeProperty ); }
    set { SetValue( () => SomeProperty, value ); }
}

1
Liên kết bị hỏng.
Guge

4

Dựa trên câu trả lời của Thomas, được chuyển thể từ câu trả lời của Marc, tôi đã biến mã thay đổi thuộc tính phản ánh thành một lớp cơ sở:

public abstract class PropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

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

    protected void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
    {
        if (selectorExpression == null)
            throw new ArgumentNullException("selectorExpression");
        var me = selectorExpression.Body as MemberExpression;

        // Nullable properties can be nested inside of a convert function
        if (me == null)
        {
            var ue = selectorExpression.Body as UnaryExpression;
            if (ue != null)
                me = ue.Operand as MemberExpression;
        }

        if (me == null)
            throw new ArgumentException("The body must be a member expression");

        OnPropertyChanged(me.Member.Name);
    }

    protected void SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression, params Expression<Func<object>>[] additonal)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return;
        field = value;
        OnPropertyChanged(selectorExpression);
        foreach (var item in additonal)
            OnPropertyChanged(item);
    }
}

Cách sử dụng giống như câu trả lời của Thomas ngoại trừ việc bạn có thể chuyển các thuộc tính bổ sung để thông báo cho. Điều này là cần thiết để xử lý các cột được tính toán cần được làm mới trong một lưới.

private int _quantity;
private int _price;

public int Quantity 
{ 
    get { return _quantity; } 
    set { SetField(ref _quantity, value, () => Quantity, () => Total); } 
}
public int Price 
{ 
    get { return _price; } 
    set { SetField(ref _price, value, () => Price, () => Total); } 
}
public int Total { get { return _price * _quantity; } }

Tôi có điều này điều khiển một bộ sưu tập các mục được lưu trữ trong BindingList được hiển thị thông qua DataGridView. Nó đã loại bỏ sự cần thiết của tôi để thực hiện các cuộc gọi Làm mới thủ công vào lưới.


4

Hãy để tôi giới thiệu cách tiếp cận của riêng tôi được gọi là Yappi . Nó thuộc về proxy proxy | trình tạo lớp dẫn xuất, thêm chức năng mới cho một đối tượng hoặc loại hiện có, như Dynamic Proxy của Caste Project.

Nó cho phép triển khai INotifyPropertyChanged một lần trong lớp cơ sở và sau đó khai báo các lớp dẫn xuất theo kiểu sau, vẫn hỗ trợ INotifyPropertyChanged cho các thuộc tính mới:

public class Animal:Concept
{
    protected Animal(){}
    public virtual string Name { get; set; }
    public virtual int Age { get; set; }
}

Sự phức tạp của lớp dẫn xuất hoặc xây dựng proxy có thể được ẩn đằng sau dòng sau:

var animal = Concept.Create<Animal>.New();

Và tất cả các công việc triển khai INotifyPropertyChanged có thể được thực hiện như thế này:

public class Concept:INotifyPropertyChanged
{
    //Hide constructor
    protected Concept(){}

    public static class Create<TConcept> where TConcept:Concept
    {
        //Construct derived Type calling PropertyProxy.ConstructType
        public static readonly Type Type = PropertyProxy.ConstructType<TConcept, Implementation<TConcept>>(new Type[0], true);
        //Create constructing delegate calling Constructor.Compile
        public static Func<TConcept> New = Constructor.Compile<Func<TConcept>>(Type);
    }


    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(PropertyChangedEventArgs eventArgs)
    {
        var caller = PropertyChanged;
        if(caller!=null)
        {
            caller(this, eventArgs);
        }
    }

    //define implementation
    public class Implementation<TConcept> : DefaultImplementation<TConcept> where TConcept:Concept
    {
        public override Func<TBaseType, TResult> OverrideGetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
        {
            return PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);
        }
        /// <summary>
        /// Overriding property setter implementation.
        /// </summary>
        /// <typeparam name="TBaseType">Base type for implementation. TBaseType must be TConcept, and inherits all its constraints. Also TBaseType is TDeclaringType.</typeparam>
        /// <typeparam name="TDeclaringType">Type, declaring property.</typeparam>
        /// <typeparam name="TConstructedType">Constructed type. TConstructedType is TDeclaringType and TBaseType.</typeparam>
        /// <typeparam name="TResult">Type of property.</typeparam>
        /// <param name="property">PropertyInfo of property.</param>
        /// <returns>Delegate, corresponding to property setter implementation.</returns>
        public override Action<TBaseType, TResult> OverrideSetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
        {
            //This code called once for each declared property on derived type's initialization.
            //EventArgs instance is shared between all events for each concrete property.
            var eventArgs = new PropertyChangedEventArgs(property.Name);
            //get delegates for base calls.
            Action<TBaseType, TResult> setter = PropertyImplementation<TBaseType, TDeclaringType>.GetSetter<TResult>(property.Name);
            Func<TBaseType, TResult> getter = PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);

            var comparer = EqualityComparer<TResult>.Default;

            return (pthis, value) =>
            {//This code executes each time property setter is called.
                if (comparer.Equals(value, getter(pthis))) return;
                //base. call
                setter(pthis, value);
                //Directly accessing Concept's protected method.
                pthis.OnPropertyChanged(eventArgs);
            };
        }
    }
}

Nó hoàn toàn an toàn để tái cấu trúc, không sử dụng phản xạ sau khi xây dựng kiểu và đủ nhanh.


Tại sao bạn cần TDeclarationloại tham số trên PropertyImplementation? Chắc chắn bạn có thể tìm thấy loại thích hợp để gọi (không phải callvirt) getter / setter từ chỉ với TImplementation?
Andrew Savinykh

TIm Hiện thực hoạt động trong hầu hết các trường hợp. Các ngoại lệ là: 1. Thuộc tính được xác định lại bằng keyvord "mới". 2. Thuộc tính của việc thực hiện giao diện rõ ràng.
Kelqualyn

3

Tất cả những câu trả lời là rất tốt đẹp.

Giải pháp của tôi là sử dụng các đoạn mã để thực hiện công việc.

Điều này sử dụng cuộc gọi đơn giản nhất đến sự kiện PropertyChanged.

Lưu đoạn mã này và sử dụng nó khi bạn sử dụng đoạn mã 'fullprop'.

vị trí có thể được tìm thấy trong menu 'Tools \ Code Snippet Manager ...' tại Visual Studio.

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>inotifypropfull</Title>
            <Shortcut>inotifypropfull</Shortcut>
            <HelpUrl>http://ofirzeitoun.wordpress.com/</HelpUrl>
            <Description>Code snippet for property and backing field with notification</Description>
            <Author>Ofir Zeitoun</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>type</ID>
                    <ToolTip>Property type</ToolTip>
                    <Default>int</Default>
                </Literal>
                <Literal>
                    <ID>property</ID>
                    <ToolTip>Property name</ToolTip>
                    <Default>MyProperty</Default>
                </Literal>
                <Literal>
                    <ID>field</ID>
                    <ToolTip>The variable backing this property</ToolTip>
                    <Default>myVar</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp">
                <![CDATA[private $type$ $field$;

    public $type$ $property$
    {
        get { return $field$;}
        set { 
            $field$ = value;
            var temp = PropertyChanged;
            if (temp != null)
            {
                temp(this, new PropertyChangedEventArgs("$property$"));
            }
        }
    }
    $end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

Bạn có thể sửa đổi cuộc gọi theo ý muốn (để sử dụng các giải pháp trên)


2

Nếu bạn đang sử dụng tính năng động trong .NET 4.5, bạn không cần phải lo lắng INotifyPropertyChanged.

dynamic obj = new ExpandoObject();
obj.Name = "John";

nếu Tên bị ràng buộc với một số điều khiển, nó chỉ hoạt động tốt.


1
bất kỳ nhược điểm của việc sử dụng này?
juFo

2

Một giải pháp kết hợp khác là sử dụng StackFrame:

public class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void Set<T>(ref T field, T value)
    {
        MethodBase method = new StackFrame(1).GetMethod();
        field = value;
        Raise(method.Name.Substring(4));
    }

    protected void Raise(string propertyName)
    {
        var temp = PropertyChanged;
        if (temp != null)
        {
            temp(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Sử dụng:

public class TempVM : BaseViewModel
{
    private int _intP;
    public int IntP
    {
        get { return _intP; }
        set { Set<int>(ref _intP, value); }
    }
}

2
Có nhanh không Không truy cập vào khung stack bị ràng buộc với một số yêu cầu cấp phép? Điều đó có mạnh mẽ trong bối cảnh sử dụng async / await không?
Stéphane Gourichon

@ StéphaneGourichon Không, không phải vậy. Truy cập khung ngăn xếp có nghĩa là một hiệu suất đáng kể trong hầu hết các trường hợp.
Bruno Brant

Có, bạn có thể thấy nó tại codereview.stackexchange.com/questions/13823/iêu
Ofir

Lưu ý rằng nội tuyến có thể ẩn get_Foophương thức trong chế độ Phát hành.
bytecode77

2

Tôi đã tạo Phương thức mở rộng trong Thư viện cơ sở để sử dụng lại:

public static class INotifyPropertyChangedExtensions
{
    public static bool SetPropertyAndNotify<T>(this INotifyPropertyChanged sender,
               PropertyChangedEventHandler handler, ref T field, T value, 
               [CallerMemberName] string propertyName = "",
               EqualityComparer<T> equalityComparer = null)
    {
        bool rtn = false;
        var eqComp = equalityComparer ?? EqualityComparer<T>.Default;
        if (!eqComp.Equals(field,value))
        {
            field = value;
            rtn = true;
            if (handler != null)
            {
                var args = new PropertyChangedEventArgs(propertyName);
                handler(sender, args);
            }
        }
        return rtn;
    }
}

Điều này hoạt động với .Net 4.5 vì CallerMemberNameAttribution . Nếu bạn muốn sử dụng nó với phiên bản .Net trước đó, bạn phải thay đổi khai báo phương thức từ: ...,[CallerMemberName] string propertyName = "", ...thành...,string propertyName, ...

Sử dụng:

public class Dog : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    string _name;

    public string Name
    {
        get { return _name; }
        set
        {
            this.SetPropertyAndNotify(PropertyChanged, ref _name, value);
        }
    }
}

2

Tôi đã giải quyết theo cách này (đó là một chút lao động, nhưng chắc chắn là nhanh hơn trong thời gian chạy).

Trong VB (xin lỗi, nhưng tôi nghĩ không khó để dịch nó bằng C #), tôi thực hiện thay thế này bằng RE:

(?<Attr><(.*ComponentModel\.)Bindable\(True\)>)( |\r\n)*(?<Def>(Public|Private|Friend|Protected) .*Property )(?<Name>[^ ]*) As (?<Type>.*?)[ |\r\n](?![ |\r\n]*Get)

với:

Private _${Name} As ${Type}\r\n${Attr}\r\n${Def}${Name} As ${Type}\r\nGet\r\nReturn _${Name}\r\nEnd Get\r\nSet (Value As ${Type})\r\nIf _${Name} <> Value Then \r\n_${Name} = Value\r\nRaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("${Name}"))\r\nEnd If\r\nEnd Set\r\nEnd Property\r\n

Transofrm này tất cả các mã như thế này:

<Bindable(True)>
Protected Friend Property StartDate As DateTime?

Trong

Private _StartDate As DateTime?
<Bindable(True)>
Protected Friend Property StartDate As DateTime?
    Get
        Return _StartDate
    End Get
    Set(Value As DateTime?)
        If _StartDate <> Value Then
            _StartDate = Value
            RaiseEvent PropertyChange(Me, New ComponentModel.PropertyChangedEventArgs("StartDate"))
        End If
    End Set
End Property

Và nếu tôi muốn có một mã dễ đọc hơn, tôi có thể ngược lại chỉ thực hiện thay thế sau:

Private _(?<Name>.*) As (?<Type>.*)[\r\n ]*(?<Attr><(.*ComponentModel\.)Bindable\(True\)>)[\r\n ]*(?<Def>(Public|Private|Friend|Protected) .*Property )\k<Name> As \k<Type>[\r\n ]*Get[\r\n ]*Return _\k<Name>[\r\n ]*End Get[\r\n ]*Set\(Value As \k<Type>\)[\r\n ]*If _\k<Name> <> Value Then[\r\n ]*_\k<Name> = Value[\r\n ]*RaiseEvent PropertyChanged\(Me, New (.*ComponentModel\.)PropertyChangedEventArgs\("\k<Name>"\)\)[\r\n ]*End If[\r\n ]*End Set[\r\n ]*End Property

Với

${Attr} ${Def} ${Name} As ${Type}

Tôi ném để thay thế mã IL của phương thức đã đặt, nhưng tôi không thể viết nhiều mã được biên dịch trong IL ... Nếu một ngày tôi viết nó, tôi sẽ nói bạn!


2

Tôi giữ điều này xung quanh như một đoạn trích. C # 6 thêm một số cú pháp hay để gọi trình xử lý.

// INotifyPropertyChanged

public event PropertyChangedEventHandler PropertyChanged;

private void Set<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(property, value) == false)
    {
        property = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

2

Đây là phiên bản Unity3D hoặc không CallerMemberName của NotifyPropertyChanged

public abstract class Bindable : MonoBehaviour, INotifyPropertyChanged
{
    private readonly Dictionary<string, object> _properties = new Dictionary<string, object>();
    private static readonly StackTrace stackTrace = new StackTrace();
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    ///     Resolves a Property's name from a Lambda Expression passed in.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="property"></param>
    /// <returns></returns>
    internal string GetPropertyName<T>(Expression<Func<T>> property)
    {
        var expression = (MemberExpression) property.Body;
        var propertyName = expression.Member.Name;

        Debug.AssertFormat(propertyName != null, "Bindable Property shouldn't be null!");
        return propertyName;
    }

    #region Notification Handlers

    /// <summary>
    ///     Notify's all other objects listening that a value has changed for nominated propertyName
    /// </summary>
    /// <param name="propertyName"></param>
    internal void NotifyOfPropertyChange(string propertyName)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }

    /// <summary>
    ///     Notifies subscribers of the property change.
    /// </summary>
    /// <typeparam name="TProperty">The type of the property.</typeparam>
    /// <param name="property">The property expression.</param>
    internal void NotifyOfPropertyChange<TProperty>(Expression<Func<TProperty>> property)
    {
        var propertyName = GetPropertyName(property);
        NotifyOfPropertyChange(propertyName);
    }

    /// <summary>
    ///     Raises the <see cref="PropertyChanged" /> event directly.
    /// </summary>
    /// <param name="e">The <see cref="PropertyChangedEventArgs" /> instance containing the event data.</param>
    internal void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    #endregion

    #region Getters

    /// <summary>
    ///     Gets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    internal T Get<T>(Expression<Func<T>> property)
    {
        var propertyName = GetPropertyName(property);
        return Get<T>(GetPropertyName(property));
    }

    /// <summary>
    ///     Gets the value of a property automatically based on its caller.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    internal T Get<T>()
    {
        var name = stackTrace.GetFrame(1).GetMethod().Name.Substring(4); // strips the set_ from name;
        return Get<T>(name);
    }

    /// <summary>
    ///     Gets the name of a property based on a string.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    internal T Get<T>(string name)
    {
        object value = null;
        if (_properties.TryGetValue(name, out value))
            return value == null ? default(T) : (T) value;
        return default(T);
    }

    #endregion

    #region Setters

    /// <summary>
    ///     Sets the value of a property whilst automatically looking up its caller name.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    internal void Set<T>(T value)
    {
        var propertyName = stackTrace.GetFrame(1).GetMethod().Name.Substring(4); // strips the set_ from name;
        Set(value, propertyName);
    }

    /// <summary>
    ///     Sets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <param name="name"></param>
    internal void Set<T>(T value, string propertyName)
    {
        Debug.Assert(propertyName != null, "name != null");
        if (Equals(value, Get<T>(propertyName)))
            return;
        _properties[propertyName] = value;
        NotifyOfPropertyChange(propertyName);
    }

    /// <summary>
    ///     Sets the value of a property based off an Expression (()=>FieldName)
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <param name="property"></param>
    internal void Set<T>(T value, Expression<Func<T>> property)
    {
        var propertyName = GetPropertyName(property);

        Debug.Assert(propertyName != null, "name != null");

        if (Equals(value, Get<T>(propertyName)))
            return;
        _properties[propertyName] = value;
        NotifyOfPropertyChange(propertyName);
    }

    #endregion
}

Mã này cho phép bạn viết các trường sao lưu thuộc tính như thế này:

  public string Text
    {
        get { return Get<string>(); }
        set { Set(value); }
    }

Hơn nữa, trong việc chia sẻ lại nếu bạn tạo một đoạn mẫu / tìm kiếm, sau đó bạn cũng có thể tự động hóa quy trình làm việc của mình bằng cách chuyển đổi các trường prop đơn giản thành sao lưu ở trên.

Mẫu tìm kiếm:

public $type$ $fname$ { get; set; }

Thay thế mẫu:

public $type$ $fname$
{
    get { return Get<$type$>(); }
    set { Set(value); }
}

2

Tôi đã viết một bài viết giúp với điều này ( https://msdn.microsoft.com/magazine/mt736453 ). Bạn có thể sử dụng gói NuSoft của SolSoft.DataBinding. Sau đó, bạn có thể viết mã như thế này:

public class TestViewModel : IRaisePropertyChanged
{
  public TestViewModel()
  {
    this.m_nameProperty = new NotifyProperty<string>(this, nameof(Name), null);
  }

  private readonly NotifyProperty<string> m_nameProperty;
  public string Name
  {
    get
    {
      return m_nameProperty.Value;
    }
    set
    {
      m_nameProperty.SetValue(value);
    }
  }

  // Plus implement IRaisePropertyChanged (or extend BaseViewModel)
}

Những lợi ích:

  1. lớp cơ sở là tùy chọn
  2. không phản ánh trên mỗi 'giá trị đặt'
  3. có thể có các thuộc tính phụ thuộc vào các thuộc tính khác và tất cả chúng đều tự động đưa ra các sự kiện thích hợp (bài viết có một ví dụ về điều này)

2

Trong khi rõ ràng có rất nhiều cách để làm điều này, ngoại trừ các câu trả lời ma thuật AOP, không có câu trả lời nào có vẻ như nhìn vào việc đặt thuộc tính của Mô hình trực tiếp từ mô hình xem mà không có trường địa phương để tham chiếu.

Vấn đề là bạn không thể tham khảo một tài sản. Tuy nhiên, bạn có thể sử dụng một Hành động để đặt thuộc tính đó.

protected bool TrySetProperty<T>(Action<T> property, T newValue, T oldValue, [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(oldValue, newValue))
    {
        return false;
    }

    property(newValue);
    RaisePropertyChanged(propertyName);
    return true;
}

Điều này có thể được sử dụng như trích xuất mã sau đây.

public int Prop {
    get => model.Prop;
    set => TrySetProperty(x => model.Prop = x, value, model.Prop);
}

Kiểm tra repo BitBucket này để thực hiện đầy đủ phương thức và một vài cách khác nhau để đạt được kết quả tương tự, bao gồm phương pháp sử dụng LINQ và phương pháp sử dụng phản xạ. Hãy lưu ý rằng các phương pháp này là hiệu suất chậm hơn khôn ngoan.


1

Những thứ khác bạn có thể muốn xem xét khi triển khai các loại thuộc tính này là thực tế là cả INotifyPropertyChang * ed * ing đều sử dụng các lớp đối số sự kiện.

Nếu bạn có một số lượng lớn các thuộc tính đang được thiết lập thì số lượng các thể hiện của lớp đối số sự kiện có thể rất lớn, bạn nên xem xét lưu trữ chúng vì chúng là một trong những khu vực có thể xảy ra vụ nổ chuỗi.

Hãy xem việc thực hiện này và giải thích lý do tại sao nó được hình thành.

Blog của Josh Smiths


1

Tôi vừa tìm thấy ActiveSharp - Automatic INotifyPropertyChanged , tôi chưa sử dụng nó, nhưng nó có vẻ tốt.

Để trích dẫn từ trang web của nó ...


Gửi thông báo thay đổi thuộc tính mà không chỉ định tên thuộc tính dưới dạng chuỗi.

Thay vào đó, hãy viết các thuộc tính như thế này:

public int Foo
{
    get { return _foo; }
    set { SetValue(ref _foo, value); }  // <-- no property name here
}

Lưu ý rằng không cần bao gồm tên của thuộc tính dưới dạng chuỗi. ActiveSharp đáng tin cậy và chính xác con số đó ra cho chính nó. Nó hoạt động dựa trên thực tế là việc triển khai thuộc tính của bạn vượt qua trường sao lưu (_foo) bằng ref. (ActiveSharp sử dụng lệnh gọi "by ref" đó để xác định trường sao lưu nào được thông qua và từ trường mà nó xác định thuộc tính).


1

Một ý tưởng sử dụng sự phản chiếu:

class ViewModelBase : INotifyPropertyChanged {

    public event PropertyChangedEventHandler PropertyChanged;

    bool Notify<T>(MethodBase mb, ref T oldValue, T newValue) {

        // Get Name of Property
        string name = mb.Name.Substring(4);

        // Detect Change
        bool changed = EqualityComparer<T>.Default.Equals(oldValue, newValue);

        // Return if no change
        if (!changed) return false;

        // Update value
        oldValue = newValue;

        // Raise Event
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }//if

        // Notify caller of change
        return true;

    }//method

    string name;

    public string Name {
        get { return name; }
        set {
            Notify(MethodInfo.GetCurrentMethod(), ref this.name, value);
        }
    }//method

}//class

Điều này là khá mát mẻ, tôi thích nó hơn là cách tiếp cận biểu hiện. Về nhược điểm, nên chậm hơn.
nawfal

1

Tôi nhận ra câu hỏi này đã có một câu trả lời đáng kinh ngạc, nhưng không ai trong số họ cảm thấy hoàn toàn đúng với tôi. Vấn đề của tôi là tôi không muốn bất kỳ bản hit hiệu suất nào và sẵn sàng đưa ra một chút chi tiết cho lý do đó một mình. Tôi cũng không quan tâm quá nhiều đến các thuộc tính tự động, điều này dẫn tôi đến giải pháp sau:

public abstract class AbstractObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

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

    protected virtual bool SetValue<TKind>(ref TKind Source, TKind NewValue, params string[] Notify)
    {
        //Set value if the new value is different from the old
        if (!Source.Equals(NewValue))
        {
            Source = NewValue;

            //Notify all applicable properties
            foreach (var i in Notify)
                OnPropertyChanged(i);

            return true;
        }

        return false;
    }

    public AbstractObject()
    {
    }
}

Nói cách khác, giải pháp trên rất tiện lợi nếu bạn không ngại làm điều này:

public class SomeObject : AbstractObject
{
    public string AnotherProperty
    {
        get
        {
            return someProperty ? "Car" : "Plane";
        }
    }

    bool someProperty = false;
    public bool SomeProperty
    {
        get
        {
            return someProperty;
        }
        set
        {
            SetValue(ref someProperty, value, "SomeProperty", "AnotherProperty");
        }
    }

    public SomeObject() : base()
    {
    }
}

Ưu

  • Không có phản xạ
  • Chỉ thông báo nếu giá trị cũ! = Giá trị mới
  • Thông báo nhiều thuộc tính cùng một lúc

Nhược điểm

  • Không có thuộc tính tự động (mặc dù bạn có thể thêm hỗ trợ cho cả hai!)
  • Một số chi tiết
  • Quyền anh (thành tích nhỏ?)

Than ôi, nó vẫn tốt hơn làm điều này,

set
{
    if (!someProperty.Equals(value))
    {
        someProperty = value;
        OnPropertyChanged("SomeProperty");
        OnPropertyChanged("AnotherProperty");
    }
}

Đối với mỗi tài sản duy nhất, trở thành một cơn ác mộng với tính dài dòng bổ sung ;-(

Lưu ý, tôi không khẳng định giải pháp này là hiệu quả tốt hơn so với các giải pháp khác, chỉ là nó là một giải pháp khả thi cho những người không thích các giải pháp khác được trình bày.


1

Tôi đã đưa ra lớp cơ sở này để triển khai mẫu có thể quan sát được, gần như thực hiện những gì bạn cần ( "tự động" thực hiện tập hợp và nhận). Tôi đã dành một giờ cho việc này như là nguyên mẫu, vì vậy nó không có nhiều bài kiểm tra đơn vị, nhưng chứng minh khái niệm này. Lưu ý rằng nó sử dụng Dictionary<string, ObservablePropertyContext>để loại bỏ sự cần thiết cho các lĩnh vực riêng tư.

  public class ObservableByTracking<T> : IObservable<T>
  {
    private readonly Dictionary<string, ObservablePropertyContext> _expando;
    private bool _isDirty;

    public ObservableByTracking()
    {
      _expando = new Dictionary<string, ObservablePropertyContext>();

      var properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList();
      foreach (var property in properties)
      {
        var valueContext = new ObservablePropertyContext(property.Name, property.PropertyType)
        {
          Value = GetDefault(property.PropertyType)
        };

        _expando[BuildKey(valueContext)] = valueContext;
      }
    }

    protected void SetValue<T>(Expression<Func<T>> expression, T value)
    {
      var keyContext = GetKeyContext(expression);
      var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);

      if (!_expando.ContainsKey(key))
      {
        throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
      }

      var originalValue = (T)_expando[key].Value;
      if (EqualityComparer<T>.Default.Equals(originalValue, value))
      {
        return;
      }

      _expando[key].Value = value;
      _isDirty = true;
    }

    protected T GetValue<T>(Expression<Func<T>> expression)
    {
      var keyContext = GetKeyContext(expression);
      var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);

      if (!_expando.ContainsKey(key))
      {
        throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
      }

      var value = _expando[key].Value;
      return (T)value;
    }

    private KeyContext GetKeyContext<T>(Expression<Func<T>> expression)
    {
      var castedExpression = expression.Body as MemberExpression;
      if (castedExpression == null)
      {
        throw new Exception($"Invalid expression.");
      }

      var parameterName = castedExpression.Member.Name;

      var propertyInfo = castedExpression.Member as PropertyInfo;
      if (propertyInfo == null)
      {
        throw new Exception($"Invalid expression.");
      }

      return new KeyContext {PropertyType = propertyInfo.PropertyType, PropertyName = parameterName};
    }

    private static string BuildKey(ObservablePropertyContext observablePropertyContext)
    {
      return $"{observablePropertyContext.Type.Name}.{observablePropertyContext.Name}";
    }

    private static string BuildKey(string parameterName, Type type)
    {
      return $"{type.Name}.{parameterName}";
    }

    private static object GetDefault(Type type)
    {
      if (type.IsValueType)
      {
        return Activator.CreateInstance(type);
      }
      return null;
    }

    public bool IsDirty()
    {
      return _isDirty;
    }

    public void SetPristine()
    {
      _isDirty = false;
    }

    private class KeyContext
    {
      public string PropertyName { get; set; }
      public Type PropertyType { get; set; }
    }
  }

  public interface IObservable<T>
  {
    bool IsDirty();
    void SetPristine();
  }

Đây là cách sử dụng

public class ObservableByTrackingTestClass : ObservableByTracking<ObservableByTrackingTestClass>
  {
    public ObservableByTrackingTestClass()
    {
      StringList = new List<string>();
      StringIList = new List<string>();
      NestedCollection = new List<ObservableByTrackingTestClass>();
    }

    public IEnumerable<string> StringList
    {
      get { return GetValue(() => StringList); }
      set { SetValue(() => StringIList, value); }
    }

    public IList<string> StringIList
    {
      get { return GetValue(() => StringIList); }
      set { SetValue(() => StringIList, value); }
    }

    public int IntProperty
    {
      get { return GetValue(() => IntProperty); }
      set { SetValue(() => IntProperty, value); }
    }

    public ObservableByTrackingTestClass NestedChild
    {
      get { return GetValue(() => NestedChild); }
      set { SetValue(() => NestedChild, value); }
    }

    public IList<ObservableByTrackingTestClass> NestedCollection
    {
      get { return GetValue(() => NestedCollection); }
      set { SetValue(() => NestedCollection, value); }
    }

    public string StringProperty
    {
      get { return GetValue(() => StringProperty); }
      set { SetValue(() => StringProperty, value); }
    }
  }

1

Tôi đề nghị sử dụng ReactiveProperty. Đây là phương pháp ngắn nhất trừ Fody.

public class Data : INotifyPropertyChanged
{
    // boiler-plate
    ...
    // props
    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }
}

thay thế

public class Data
{
    // Don't need boiler-plate and INotifyPropertyChanged

    // props
    public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>();
}

( DOCS )


0

Một ý tưởng khác...

 public class ViewModelBase : INotifyPropertyChanged
{
    private Dictionary<string, object> _propertyStore = new Dictionary<string, object>();
    protected virtual void SetValue<T>(T value, [CallerMemberName] string propertyName="") {
        _propertyStore[propertyName] = value;
        OnPropertyChanged(propertyName);
    }
    protected virtual T GetValue<T>([CallerMemberName] string propertyName = "")
    {
        object ret;
        if (_propertyStore.TryGetValue(propertyName, out ret))
        {
            return (T)ret;
        }
        else
        {
            return default(T);
        }
    }

    //Usage
    //public string SomeProperty {
    //    get { return GetValue<string>();  }
    //    set { SetValue(value); }
    //}

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        var temp = PropertyChanged;
        if (temp != null)
            temp.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

0

=> đây là giải pháp của tôi với các tính năng sau

 public ResourceStatus Status
 {
     get { return _status; }
     set
     {
         _status = value;
         Notify(Npcea.Status,Npcea.Comments);
     }
 }
  1. không giới thiệu
  2. ký hiệu ngắn
  3. không có chuỗi ma thuật trong mã doanh nghiệp của bạn
  4. Khả năng sử dụng lại của PropertyChangedEventArss trên ứng dụng
  5. Khả năng thông báo nhiều thuộc tính trong một tuyên bố

0

Dùng cái này

using System;
using System.ComponentModel;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;


public static class ObservableFactory
{
    public static T Create<T>(T target)
    {
        if (!typeof(T).IsInterface)
            throw new ArgumentException("Target should be an interface", "target");

        var proxy = new Observable<T>(target);
        return (T)proxy.GetTransparentProxy();
    }
}

internal class Observable<T> : RealProxy, INotifyPropertyChanged, INotifyPropertyChanging
{
    private readonly T target;

    internal Observable(T target)
        : base(ImplementINotify(typeof(T)))
    {
        this.target = target;
    }

    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = msg as IMethodCallMessage;

        if (methodCall != null)
        {
            return HandleMethodCall(methodCall);
        }

        return null;
    }

    public event PropertyChangingEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;



    IMessage HandleMethodCall(IMethodCallMessage methodCall)
    {
        var isPropertySetterCall = methodCall.MethodName.StartsWith("set_");
        var propertyName = isPropertySetterCall ? methodCall.MethodName.Substring(4) : null;

        if (isPropertySetterCall)
        {
            OnPropertyChanging(propertyName);
        }

        try
        {
            object methodCalltarget = target;

            if (methodCall.MethodName == "add_PropertyChanged" || methodCall.MethodName == "remove_PropertyChanged"||
                methodCall.MethodName == "add_PropertyChanging" || methodCall.MethodName == "remove_PropertyChanging")
            {
                methodCalltarget = this;
            }

            var result = methodCall.MethodBase.Invoke(methodCalltarget, methodCall.InArgs);

            if (isPropertySetterCall)
            {
                OnPropertyChanged(methodCall.MethodName.Substring(4));
            }

            return new ReturnMessage(result, null, 0, methodCall.LogicalCallContext, methodCall);
        }
        catch (TargetInvocationException invocationException)
        {
            var exception = invocationException.InnerException;
            return new ReturnMessage(exception, methodCall);
        }
    }

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

    protected virtual void OnPropertyChanging(string propertyName)
    {
        var handler = PropertyChanging;
        if (handler != null) handler(this, new PropertyChangingEventArgs(propertyName));
    }

    public static Type ImplementINotify(Type objectType)
    {
        var tempAssemblyName = new AssemblyName(Guid.NewGuid().ToString());

        var dynamicAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
            tempAssemblyName, AssemblyBuilderAccess.RunAndCollect);

        var moduleBuilder = dynamicAssembly.DefineDynamicModule(
            tempAssemblyName.Name,
            tempAssemblyName + ".dll");

        var typeBuilder = moduleBuilder.DefineType(
            objectType.FullName, TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract);

        typeBuilder.AddInterfaceImplementation(objectType);
        typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanged));
        typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanging));
        var newType = typeBuilder.CreateType();
        return newType;
    }
}

}

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.