2011-10-11 26 views
15

thể trùng lặp:
typesafe NotifyPropertyChanged using linq expressionsthực hiện NotifyPropertyChanged không dây ma thuật

Tôi đang làm việc trên một ứng dụng nhóm lớn mà đang phải chịu đựng việc sử dụng nặng của dây ma thuật theo hình thức NotifyPropertyChanged("PropertyName"), - việc thực hiện tiêu chuẩn khi tư vấn cho Microsoft. Chúng tôi cũng đang phải chịu một số lượng lớn các thuộc tính bị đặt tên (làm việc với một mô hình đối tượng cho một mô-đun tính toán có hàng trăm thuộc tính được lưu trữ) - tất cả đều được ràng buộc với giao diện người dùng.

Nhóm của tôi gặp nhiều lỗi liên quan đến thay đổi tên thuộc tính dẫn đến chuỗi ma thuật không chính xác và ràng buộc phá vỡ. Tôi muốn giải quyết vấn đề bằng cách thực hiện các thông báo thay đổi thuộc tính mà không sử dụng các chuỗi ma thuật. Các giải pháp duy nhất tôi đã tìm thấy cho .Net 3.5 liên quan đến các biểu thức lambda. (Ví dụ: Implementing INotifyPropertyChanged - does a better way exist?)

quản lý của tôi là vô cùng lo lắng về chi phí thực hiện chuyển đổi từ

set { ... OnPropertyChanged("PropertyName"); } 

để

set { ... OnPropertyChanged(() => PropertyName); } 

nơi tên này được chiết xuất từ ​​

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

Xem xét một ứng dụng như bảng tính khi thông số thay đổi, khoảng một trăm giá trị được tính toán lại và cập nhật trên giao diện người dùng trong thời gian thực. Việc thực hiện thay đổi này có tốn kém đến nỗi nó sẽ tác động đến sự đáp ứng của UI? Tôi thậm chí không thể biện minh cho thử nghiệm thay đổi này ngay bây giờ bởi vì nó sẽ mất khoảng 2 ngày giá trị của việc cập nhật tài sản setters trong các dự án và các lớp học.

+0

Tôi sử dụng phản ánh cho điều này. Xem bài đăng trên blog của tôi ở đây về điều này. [http://tsells.wordpress.com/2011/02/08/using-reflection-with-wpf-and-the-inotifypropertychanged-interface/](http://tsells.wordpress.com/2011/02/08/using-reflection-with-wpf-and-the-inotifypropertychanged-interface /) Hãy chú ý đến lưu ý hiệu suất ở cuối bài đăng. – tsells

+0

Bài viết hay, nhưng bạn đã đưa ra sự khác biệt tuyệt đối về thời gian biểu diễn, nhưng điều đó không hữu ích. Tôi muốn được nhiều hơn nữa quan tâm đến sự khác biệt tỷ lệ phần trăm trong thời gian thực hiện. Có sự khác biệt lớn giữa việc chuyển từ 200 mili giây sang 300 mili giây, và từ 0,01 mili giây đến 100,01 mili giây. Cùng một sự khác biệt tuyệt đối, khác biệt tỷ lệ phần trăm khác nhau. – Alain

+0

Ông cho biết sự khác biệt là khoảng 1/4 giây cho 10.000 thông báo thay đổi tài sản. Tôi không nghĩ rằng đó là một sự khác biệt đủ lớn để quan tâm, và nếu bạn thực sự đang cập nhật hơn 10k tài sản cùng một lúc tôi nghiêm túc xem xét lại thiết kế :) – Rachel

Trả lời

22

Tôi đã làm một bài kiểm tra kỹ lưỡng của NotifyPropertyChanged để thiết lập ảnh hưởng của việc chuyển sang các biểu thức lambda.

Dưới đây là kết quả xét nghiệm của tôi:

enter image description here

Như bạn thấy, sử dụng biểu thức lambda là chậm hơn so với vùng đồng bằng mã hóa cứng thực hiện chuỗi thay đổi sở hữu khoảng 5 lần, nhưng người dùng không nên băn khoăn , bởi vì thậm chí sau đó nó có khả năng bơm ra một trăm nghìn thay đổi tài sản mỗi giây trên máy tính không hoạt động đặc biệt của tôi. Như vậy, lợi ích thu được từ việc không còn phải sử dụng các chuỗi mã cứng và có thể có những người định cư một dòng để chăm sóc cho tất cả doanh nghiệp của bạn vượt xa chi phí hiệu suất đối với tôi.

Test 1 sử dụng thực hiện setter tiêu chuẩn, với một tấm séc để thấy rằng bất động sản đã thực sự thay đổi:

public UInt64 TestValue1 
    { 
     get { return testValue1; } 
     set 
     { 
      if (value != testValue1) 
      { 
       testValue1 = value; 
       InvokePropertyChanged("TestValue1"); 
      } 
     } 
    } 

thử nghiệm 2 là rất giống nhau, với việc bổ sung một tính năng cho phép sự kiện để theo dõi giá trị cũ và giá trị mới.Bởi vì tính năng này sẽ là tiềm ẩn trong phương pháp cơ sở setter mới của tôi, tôi muốn xem có bao nhiêu của overhead mới là do tính năng:

public UInt64 TestValue2 
    { 
     get { return testValue2; } 
     set 
     { 
      if (value != testValue2) 
      { 
       UInt64 temp = testValue2; 
       testValue2 = value; 
       InvokePropertyChanged("TestValue2", temp, testValue2); 
      } 
     } 
    } 

thử nghiệm 3 là nơi cao su gặp đường, và tôi nhận được để khoe cú pháp đẹp mới này để thực hiện tất cả các hành động bất động sản có thể quan sát trong một dòng:

public UInt64 TestValue3 
    { 
     get { return testValue3; } 
     set { SetNotifyingProperty(() => TestValue3, ref testValue3, value); } 
    } 

thực hiện

trong BindingObjectBa của tôi se class, mà tất cả các ViewModels kết thúc kế thừa, nằm trong việc thực hiện lái xe tính năng mới. Tôi đã tước ra xử lý nên thịt của hàm lỗi được rõ ràng:

protected void SetNotifyingProperty<T>(Expression<Func<T>> expression, ref T field, T value) 
{ 
    if (field == null || !field.Equals(value)) 
    { 
     T oldValue = field; 
     field = value; 
     OnPropertyChanged(this, new PropertyChangedExtendedEventArgs<T>(GetPropertyName(expression), oldValue, value)); 
    } 
} 
protected string GetPropertyName<T>(Expression<Func<T>> expression) 
{ 
    MemberExpression memberExpression = (MemberExpression)expression.Body; 
    return memberExpression.Member.Name; 
} 

Cả ba phương pháp gặp nhau tại các thói quen OnPropertyChanged, đó vẫn là tiêu chuẩn:

public virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e) 
{ 
    PropertyChangedEventHandler handler = PropertyChanged; 
    if (handler != null) 
     handler(sender, e); 
} 

Bonus

Nếu có ai đó tò mò, PropertyChangedExtendedEventArgs là một cái gì đó tôi chỉ cần đưa ra để mở rộng các tiêu chuẩn PropertyChangedEventArgs, do đó, một thể hiện của phần mở rộng luôn luôn có thể được ở vị trí của cơ sở. Nó tận dụng kiến ​​thức về giá trị cũ khi một thuộc tính được thay đổi bằng cách sử dụng SetNotifyingProperty và làm cho thông tin này có sẵn cho trình xử lý.

public class PropertyChangedExtendedEventArgs<T> : PropertyChangedEventArgs 
{ 
    public virtual T OldValue { get; private set; } 
    public virtual T NewValue { get; private set; } 

    public PropertyChangedExtendedEventArgs(string propertyName, T oldValue, T newValue) 
     : base(propertyName) 
    { 
     OldValue = oldValue; 
     NewValue = newValue; 
    } 
} 
+0

Thú vị, tôi đã chạy một thử nghiệm tương tự trên máy tính của tôi một thời gian trước (2013) và không thể đo lường bất kỳ sự khác biệt hiệu suất nào (.NET 3.5 SP1, Win7 x64, Core i7) – ChrisWue

+0

@Alain - có thể được cải thiện cho đến phiên bản đẹp hơn bằng cách sử dụng new [CallerMemberName] thuộc tính - SetNotifyingProperty (ref lưu trữ, giá trị); – Axarydax

+0

Vâng tôi nghĩ rằng đây là một mô hình đủ phổ biến mà MS đã làm việc trên những cách hiệu quả hơn để có được tên thuộc tính trong các phiên bản .NET mới nhất. Tôi sẽ đề nghị rằng điều này là tốt như nó được chỉ cho các phiên bản 3.5 hoặc 4.0. – Alain

3

Cá nhân tôi thích sử dụng Microsoft PRISM NotificationObject vì lý do này và tôi đoán mã của họ được tối ưu hóa hợp lý vì nó được tạo bởi Microsoft.

Nó cho phép tôi sử dụng mã như RaisePropertyChanged(() => this.Value);, ngoài việc giữ "Magic Strings" để bạn không phá vỡ bất kỳ mã hiện có nào.

Nếu tôi nhìn vào mã của họ với Reflector, thực hiện của họ có thể được tái tạo với mã dưới đây

public class ViewModelBase : INotifyPropertyChanged 
{ 
    // Fields 
    private PropertyChangedEventHandler propertyChanged; 

    // Events 
    public event PropertyChangedEventHandler PropertyChanged 
    { 
     add 
     { 
      PropertyChangedEventHandler handler2; 
      PropertyChangedEventHandler propertyChanged = this.propertyChanged; 
      do 
      { 
       handler2 = propertyChanged; 
       PropertyChangedEventHandler handler3 = (PropertyChangedEventHandler)Delegate.Combine(handler2, value); 
       propertyChanged = Interlocked.CompareExchange<PropertyChangedEventHandler>(ref this.propertyChanged, handler3, handler2); 
      } 
      while (propertyChanged != handler2); 
     } 
     remove 
     { 
      PropertyChangedEventHandler handler2; 
      PropertyChangedEventHandler propertyChanged = this.propertyChanged; 
      do 
      { 
       handler2 = propertyChanged; 
       PropertyChangedEventHandler handler3 = (PropertyChangedEventHandler)Delegate.Remove(handler2, value); 
       propertyChanged = Interlocked.CompareExchange<PropertyChangedEventHandler>(ref this.propertyChanged, handler3, handler2); 
      } 
      while (propertyChanged != handler2); 
     } 
    } 

    protected void RaisePropertyChanged(params string[] propertyNames) 
    { 
     if (propertyNames == null) 
     { 
      throw new ArgumentNullException("propertyNames"); 
     } 
     foreach (string str in propertyNames) 
     { 
      this.RaisePropertyChanged(str); 
     } 
    } 

    protected void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression) 
    { 
     string propertyName = PropertySupport.ExtractPropertyName<T>(propertyExpression); 
     this.RaisePropertyChanged(propertyName); 
    } 

    protected virtual void RaisePropertyChanged(string propertyName) 
    { 
     PropertyChangedEventHandler propertyChanged = this.propertyChanged; 
     if (propertyChanged != null) 
     { 
      propertyChanged(this, new PropertyChangedEventArgs(propertyName)); 
     } 
    } 
} 

public static class PropertySupport 
{ 
    // Methods 
    public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression) 
    { 
     if (propertyExpression == null) 
     { 
      throw new ArgumentNullException("propertyExpression"); 
     } 
     MemberExpression body = propertyExpression.Body as MemberExpression; 
     if (body == null) 
     { 
      throw new ArgumentException("propertyExpression"); 
     } 
     PropertyInfo member = body.Member as PropertyInfo; 
     if (member == null) 
     { 
      throw new ArgumentException("propertyExpression"); 
     } 
     if (member.GetGetMethod(true).IsStatic) 
     { 
      throw new ArgumentException("propertyExpression"); 
     } 
     return body.Member.Name; 
    } 
} 
+0

PRISM đang làm gì với các phương thức thêm/xóa sự kiện PropertyChanged? Dù sao, có vẻ như họ đang thực hiện chính xác những gì tôi có ở trên: 'Biểu hiện body.Member.Name.' Không giúp với câu hỏi hiệu suất mặc dù. PRISM rõ ràng không phải là hiệu suất được tối ưu hóa - nếu nó đã được họ sẽ sử dụng lại biến được xác định của họ ''member'' thay vì gọi" 'body.Member'" lần thứ hai ở dưới cùng của phương thức 'ExtractPropertyName' của họ. – Alain

+0

Một số người cho rằng "Mã xxx được tối ưu hóa nhất có thể bởi vì nó là của Microsoft" là một oxymoron. Tôi không nghĩ đó là sự thật, nhưng sự thật _does_ nằm ở đâu đó ở giữa. –

+0

@Alain Nó cũng có thể được phản ánh không chính xác. Tôi đã phải thực hiện một số chỉnh nhỏ để mã để có được nó để biên dịch.Nó có thể dễ dàng thực hiện một thay đổi như thế này cho lớp cơ sở của bạn, chạy thử nghiệm hiệu năng w/Magic Strings của bạn, sau đó thực hiện tìm/thay thế bằng Regular Expressions để thay thế các cuộc gọi PropertyChange bằng lambda mới và chạy lại Kiểm tra hiệu năng. – Rachel

1

Trên thực tế chúng tôi đã thảo luận là tốt này cho các dự án của chúng tôi và nói rất nhiều về những ưu và khuyết điểm. Cuối cùng, chúng tôi quyết định giữ nguyên phương pháp thông thường nhưng sử dụng một trường cho nó.

public class MyModel 
{ 
    public const string ValueProperty = "Value"; 

    public int Value 
    { 
     get{return mValue;} 
     set{mValue = value; RaisePropertyChanged(ValueProperty); 
    } 
} 

Điều này giúp khi refactoring, giữ hiệu suất của chúng tôi và đặc biệt hữu ích khi chúng ta sử dụng PropertyChangedEventManager, nơi chúng ta sẽ cần các chuỗi mã hóa cứng lại.

public bool ReceiveWeakEvent(Type managerType, object sender, System.EventArgs e) 
{ 
    if(managerType == typeof(PropertyChangedEventManager)) 
    { 
     var args = e as PropertyChangedEventArgs; 
     if(sender == model) 
     { 
      if (args.PropertyName == MyModel.ValueProperty) 
      { 

      } 

      return true; 
     } 
    } 
} 
+0

Oh đến, khi bạn downvote ít nhất là nói lý do tại sao. Câu trả lời của tôi là một lựa chọn hợp lệ cho vấn đề. Sử dụng các chuỗi Hardcoded chống lại phép thuật Lambda, có thể làm tổn thương hiệu suất. Giải pháp của tôi không làm tổn thương hiệu suất và dễ bảo trì hơn so với các chuỗi được mã hóa cứng. – dowhilefor

+1

Tôi không downvote, nhưng điều này không hữu ích. Nó vẫn liên quan đến các chuỗi mã hóa cứng. Nó tệ hơn, trên thực tế, bởi vì nếu tôi có 100 thuộc tính, nó yêu cầu 100 chuỗi mới để lưu trữ những tên đó, và bây giờ khi tôi thay đổi tên của thuộc tính đó, tôi phải đổi tên, chuỗi mã hóa cứng và tên chuỗi mã hóa cứng. – Alain

+0

Đối với chúng tôi, việc đổi tên không bao giờ là một mối quan tâm, có thể vì chúng tôi sử dụng chức năng chia sẻ lại và chúng tôi hơi hư hỏng vì nó. Thứ hai, các mô hình của chúng tôi được tạo ra, do đó tất cả các thuộc tính có thể dễ dàng nhận được một mã định danh. Cuối cùng nhưng không kém phần quan trọng, hiệu suất là phần quan trọng nhất của chúng tôi, tất nhiên nó không đẹp như điều lambda nhưng đối với chúng tôi nó hoạt động khá tốt. Đặc biệt là một phần về WeakEventManager. Vì vậy, cuối cùng tôi nghĩ rằng một cách tiếp cận khác nhau có thể ít nhất là hữu ích để xem xét và trọng lượng những thuận và chống của các giải pháp trong tầm tay. Nhưng tôi tôn trọng nếu nó không hữu ích cho bạn. – dowhilefor

2

Nếu bạn lo ngại rằng giải pháp lambda-expression-tree có thể quá chậm, hãy lập hồ sơ và tìm hiểu. Tôi nghi ngờ thời gian dành nứt mở cây biểu hiện sẽ hơi nhỏ hơn một chút so với lượng thời gian giao diện người dùng sẽ làm mới để trả lời.

Nếu bạn thấy rằng nó là quá chậm, và bạn cần phải sử dụng các chuỗi chữ để đáp ứng tiêu chuẩn hiệu suất của bạn, thì đây là một cách tiếp cận tôi đã nhìn thấy:

Tạo một lớp cơ sở mà thực hiện INotifyPropertyChanged, và cung cấp cho nó phương thức RaisePropertyChanged. Phương thức đó kiểm tra xem sự kiện có là null hay không, tạo ra PropertyChangedEventArgs và kích hoạt sự kiện - tất cả các công cụ thông thường.

Nhưng phương pháp này cũng chứa một số chẩn đoán bổ sung - nó thực hiện một số Phản ánh để đảm bảo rằng lớp thực sự có thuộc tính với tên đó. Nếu thuộc tính không tồn tại, nó sẽ ném một ngoại lệ. Nếu thuộc tính không tồn tại, thì nó sẽ ghi nhớ kết quả đó (ví dụ: bằng cách thêm tên thuộc tính vào một static HashSet<string>), do đó, nó không phải thực hiện lại việc kiểm tra Reflection.

Và ở đó bạn đi: các thử nghiệm tự động của bạn sẽ bắt đầu thất bại ngay khi bạn đổi tên thuộc tính nhưng không cập nhật chuỗi phép thuật. (Tôi giả sử bạn có các thử nghiệm tự động cho Chế độ xem của mình, vì đó là lý do chính để sử dụng MVVM.)

Nếu bạn không muốn thất bại hoàn toàn trong sản xuất, bạn có thể đặt mã chẩn đoán bổ sung bên trong #if DEBUG .

+0

Đề xuất tốt trong trường hợp cây biểu hiện lambda không phát ra. – Alain

1

Một giải pháp đơn giản là xử lý trước tất cả các tệp trước khi biên dịch, phát hiện các cuộc gọi OnPropertyChanged được xác định trong khối {...}, xác định tên thuộc tính và sửa tham số tên cho phù hợp.

Bạn có thể thực hiện điều này bằng cách sử dụng công cụ đặc biệt (có thể là đề xuất của tôi) hoặc sử dụng trình phân tích cú pháp C# (hoặc VB.NET) thực (như những gì có thể tìm thấy ở đây: Parser for C#).

Tôi nghĩ đó là cách hợp lý để thực hiện điều đó. Tất nhiên, nó không phải là rất thanh lịch cũng không thông minh, nhưng nó có tác động thời gian chạy bằng không, và tuân thủ các quy tắc của Microsoft.

Nếu bạn muốn tiết kiệm một số thời gian biên dịch, bạn có thể có cả hai cách sử dụng chỉ thị biên soạn, như thế này:

set 
{ 
#if DEBUG // smart and fast compile way 
    OnPropertyChanged(() => PropertyName); 
#else // dumb but efficient way 
    OnPropertyChanged("MyProp"); // this will be fixed by buid process 
#endif 
} 
Các vấn đề liên quan