2016-02-20 38 views
-2

我有一個實現屬性更改通知的基類。爲了清楚起見,我省略了實施技術細節。使用屬性更改通知和繼承

public class PersonDTO : INotifyPropertyChanged { 

    // in real these all have the backing field + equality check in setter + OnChanged call implementations 
    public string Name { get; set; } 
    public int Age { get; set; } 
    public Gender Gender { get; set; } 

    public PersonDTO() { 
    // initialize default values 
    // this invoke OnChanged so the object state can be maintained 
    Name = "New person"; 
    Age = 30; 
    Gender = Gender.Female; 
    } 

    protected virtual void OnChanged(string propertyName) { 
    // raise PropertyChanged 
    } 
} 

我有另一個類從PersonDTO繼承,並添加了一些屬性。

public class PersonEditorModel : PersonDTO { 

    public BindingList<string> Titles { get; private set; } 

    private readonly IRepository _repository; 
    public PersonEditorModel(IRepository repository) { 
    _repository = repository; 
    } 

    protected override void OnChanged(string propertyname) { 
    if (propertyName == "Gender") { 
     // Here is a NullReferenceException 
     Titles.Clear(); 
     if (Gender == Gender.Female) { 
     Titles.AddRange(new[] {"Ms", "Mrs"}); 
     else 
     Titles.AddRange(new[] {"Mr", "Sir"}); 
    } 
    // do some other things perhaps using the _repository (which would raise a NullReferenceException again) 
    } 
} 

與該模型的問題是,在基體的構造,設置一個屬性調用改變通知,並且當所述後代尚未構造(Titles列表爲空)的子孫類的OnChanged方法執行。

我一直在想幾種方法。

  1. 在基礎構造函數中使用後備字段。這將修復異常,但對象狀態不會相應刷新。我始終需要一致的狀態,這意味着GenderTitles應該同步。

  2. 包含一個標誌,表示該對象已構建,並檢查OnChanged中的該標誌。這對於這樣的簡單情況是有效的,但是如果我有3級的層次結構會怎麼樣。我需要確保在最底層的構造函數完成運行時設置該標誌,這不是微不足道的。

  3. 在基類中使用工廠方法模式,在構建後我會調用類似StartChangeTracking()的東西。這是有問題的,因爲後代類可以有不同的構造函數參數,例如在這種情況下IRepository服務。此外,工廠方法模式會使例如Json序列化/反序列化相當困難(我的意思是那些沒有構造函數參數的類)。

+1

你在哪裏初始化標題?你不能簡單地在構造函數中初始化它或者像這樣:public BindingList Titles {get;私人設置; } = new BindingList (); – 3615

+0

我在構造函數中初始化它,但問題是父構造函數更早運行。關於後一種語法,我認爲它僅適用於目前無法使用的C#6,我現在必須堅持使用.NET 4.0。另外,我不知道它在C#6中編譯的內容,可能它仍然會在構造函數中初始化,或者初始化程序將會轉到後臺字段,在這種情況下,它確實是一個解決方案。 –

+1

@ZoltánTamási:C#6.0不僅限於.NET 4.5。即使對於.NET 2.0或更早版本,您也可以使用C#6.0。 C#6.0是一個編譯器。您仍然可以使用Visual Studio 2015並定位舊框架,並且仍然使用C#6.0功能,只要它們不需要.NET 4.5功能,請參閱http://stackoverflow.com/a/28921749/455493 – Tseng

回答

0

我終於意識到要解決這個問題,我必須解決間接導致這種行爲的根本問題。根本問題是構造函數中的虛擬成員調用(在這種情況下以間接方式)。

因此,我決定根本不使用虛擬OnChanged方法,而是自行訂閱對象自己的PropertyChanged事件。自行訂閱應始終安全,無需取消訂閱。解決方案看起來像這樣。

public class PersonEditorModel : PersonDTO { 

    public BindingList<string> Titles { get; private set; } 

    private readonly IRepository _repository; 
    public PersonEditorModel(IRepository repository) { 
    _repository = repository; 
    Titles = new BindingList<string>(); 

    UpdateTitles(); 
    PropertyChanged += OnPropertyChanged; 
    } 

    private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { 
    if (e.PropertyName == "Gender") { 
     UpdateTitles(); 
    } 
    } 

    private void UpdateTitles() { 
    Titles.Clear(); 
    if (Gender == Gender.Female) { 
     Titles.AddRange(new[] {"Ms", "Mrs"}); 
    else 
     Titles.AddRange(new[] {"Mr", "Sir"}); 
    } 
} 
1

你在這裏有幾個選擇。首先,您可以直接指定Titles(如下所示),假設您不會重新指定BindingList<T>(通常在MVVM中,我們使用的是ObservableCollection<T>)但我已將其設置爲只讀。這保證了Titles永遠不會爲空,你會不會做null -checks

// C# 6.0, read only property 
public BindingList<string> Titles { get; } = new BindingList<string>(); 

// C# 5.0 and older 
private readonly BindingList<string> titles = new BindingList<string>(); 
public BindingList<string> Titles { get { return titles; } } 

其他(非最佳的)當然選項包括裏面做了OnChange法空檢查。

 protected override void OnChanged(string propertyName) 
     { 
      if (propertyName == "Gender") 
      { 
       if(Titles==null) 
       { 
        Titles = new BindingList<string>(); 
       } 
       Titles.Clear(); 
       if (Gender == Gender.Female) 
       { 
        //Titles.AddRange(new[] { "Ms", "Mrs" }); 
        Titles.Add("Ms"); 
        Titles.Add("Mrs"); 
       } 
       else 
       { 
        Titles.Add("Mr"); 
        Titles.Add("Sir"); 
       } 
      } 

      base.OnChanged(propertyName); 
     } 

不太理想,因爲這取決於您可以在另一個分支,其中Titles可能是空最終的執行順序上,必須添加額外的空檢查和分配。此外,當你的父構造函數完成執行時,它將執行你的孩子的構造函數,並且Titles將被分配。如果您再次在此處重新分配,則會覆蓋OnChanged內完成的分配。

最後但並非最不重要的,你可以做懶惰的實例。如果您訪問的財產,你沒有初始化它在構造函數中

private BindingList<string> titles; 
public BindingList<string> Titles { 
    get 
    { 
     if(titles == null) 
     { 
      titles = new BindingList<string>(); 
     } 

     return titles; 
    } 
} 

這樣,Title總是會返回一個實例。

您應該使用我上面發佈的第一個選項,因爲它是遵循最佳實踐和列表及其他對象的最佳實踐。如果您需要運行時參數來實例化它,則只有構造函數或惰性實例化作爲選項。

您應該避免在公共api(接口,類等)中使用Initialize()類型的方法,因爲這被認爲是代碼異味,因爲調用此方法對於類的正確功能來說不是必需的。

P.S.根據您的OnChange調用的實施情況,您可能會也可能不會收到覆蓋類OnChange類中的「性別」事件。我認爲Genderenum,如果它被定義爲以下

public enum Gender 
{ 
    Female, 
    Male 
} 

和你打電話Gender = Gender.Female構造函數裏面,它可能不會觸發OnChange方法,如果你的代碼看起來像

public Gender 
{ 
    get { return gender; } 
    set 
    { 
     if(gender!=value) 
     { 
      gender = value; 
      OnChange("Gender"); 
     } 
    } 
} 
+0

太好了,謝謝你的精彩彙總。 –

1

提供的解決方案的工作,但我認爲建築甚至視圖模型本身,可以提高如下圖所示:

1)數據模型與服務模型/視圖模型分離 - 已經提到一個評論,但在這裏舉例說明。視圖模型(您的「編輯器」類)應儘可能與數據模型儘可能鬆散耦合,因此繼承是一種不可否認的方式。另外,DI建議是prefer composition over inheritance,儘管可能會使用一些tricks

2)喜歡靜態鍵入魔法字符串。例如e.PropertyName == "Gender"易受通過重構(自動)完成的屬性名稱更改的影響,但無法更改這些字符串。

public enum Gender 
{ 
    Male, 
    Female 
}; 

// this should be a simple class (POCO), persistence agnostic 
public class PersonDTO 
{ 
    public string Name { get; set; } 
    public int Age { get; set; } 
    public Gender Gender { get; set; } 
} 

// repository interface 
public interface IRepository<T> 
{ 
    IQueryable<T> GetAll(); 
    T GetById(int id); 
} 

// this is responsible for delivering person related information without exposing fetching details 
public class PersonService 
{ 
    private IRepository<PersonDTO> _Repository; 

    public PersonService(IRepository<PersonDTO> repository) 
    { 
     _Repository = repository; 
    } 

    // normally, service should return service models that are view agnostic, but this requires extra mapping 
    // so, for convenience service returns the view model 
    public PersonEditorModel GetPerson(int id) 
    { 
     var ret = AutoMapper.Mapper.Map<PersonEditorModel>(_Repository.GetById(id)); 
     return ret; 
    } 
} 

// base editor model (or view model) 
public class BaseEditorModel : INotifyPropertyChanged 
{ 
    /// <summary> 
    /// Occurs when a property value changes. 
    /// </summary> 
    public event PropertyChangedEventHandler PropertyChanged; 

    /// <summary> 
    /// Raises the PropertyChanged event 
    /// </summary> 
    /// <param name="propertyName">Name of the property</param> 
    protected void OnPropertyChanged(string propertyName) 
    { 
     var ev = PropertyChanged; 
     if (ev != null) 
      PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 
    } 
} 

// base editor model (or view model) that allow statically-typed property changed notifications 
public abstract class BaseEditorModel<TVm> : BaseEditorModel 
           where TVm : BaseEditorModel<TVm> 
{ 
    /// <summary> 
    /// Raises the PropertyChanged event 
    /// </summary> 
    /// <param name="expr">Lambda expression that identifies the updated property</param> 
    protected void OnPropertyChanged<TProp>(Expression<Func<TVm, TProp>> expr) 
    { 
     var prop = (MemberExpression)expr.Body; 
     OnPropertyChanged(prop.Member.Name); 
    } 
} 

// the actual editor 
// notice that property changed is done directly on setters and without magic strings 
public class PersonEditorModel : BaseEditorModel<PersonEditorModel> 
{ 
    public BindingList<string> Titles { get; private set; } 

    public PersonEditorModel() 
    { 
     Titles = new BindingList<string>(); 

     UpdateTitles(); 
    } 

    private Gender _Gender; 
    public Gender Gender 
    { 
     get { return _Gender; } 
     set 
     { 
      _Gender = value; 
      UpdateTitles(); 
      OnPropertyChanged(m => m.Gender); 
     } 
    } 

    private void UpdateTitles() 
    { 
     Titles = Gender == Gender.Female ? 
      new BindingList<string>(new[] { "Ms", "Mrs" }) : 
      new BindingList<string>(new[] { "Mr", "Sir" }); 
     OnPropertyChanged(m => m.Titles); 
    } 
} 

// just an example 
class Program 
{ 
    static void Main(string[] args) 
    { 
     // this should be performed one per application run 
     // obsolete in a newer version 
     AutoMapper.Mapper.CreateMap<PersonDTO, PersonEditorModel>(); 

     var service = new PersonService(null);  // get service using DI 

     // work on a dummy/mock person 
     var somePerson = service.GetPerson(30); 

     // bind and do stuff with person view model 
    } 
} 
+0

謝謝你的回答,你絕對有好點。然而,當項目有一個WinForms前端時,我很久以前就選擇了基於繼承的結構。將編輯器模型傳遞給接受相應數據模型的服務層非常簡單。在網絡世界中,事情是完全不同的,在一個新的項目中,我認爲我會選擇合成。然而,說實話,這種基於繼承的技術即使在Web應用程序中也沒有任何醜陋的變通方法,並且在一天結束時比起當前的趨勢更重要。 –

+0

至於魔術串,這只是爲了舉例。在我的真實代碼中,我使用基於表達式的語法和一些有用的擴展方法來解決重構問題。 –

+0

@ZoltánTamási - 是的,它取決於體系結構,但是在更大的項目中,單元測試或要求移植到其他框架(例如,從Windows Forms直接連接到數據庫到3層體系結構中的WPF應用程序)。在這種情況下,開銷會得到回報。 – Alexei