January 20, 2014

.NET內建的Observer Pattern應用

在一個Windows Forms專案中,有個需求如下。

主表單有一DataGridView顯示會員資料列表,使用者點選某筆會員資料,系統會跳出一個子表單提供修改會員資料功能。更新會員資料後,主表單DataGridView裡的會員資料也隨之更新,以反應最新會員資料。




依上所述,可能的實作方式可以在主表單加入一個取得最新會員資料的方法,如LoadLatestUsers。在子表單中更新會員資料後,呼叫主表單的LoadLatestUsers以更新主表單的顯示資料。

主表單
private void DataGridView_Users_CellMouseDoubleClick(object sender, DataGridViewCellMouseEventArgs e)
{
    if (e.RowIndex == -1)
    {
        return;
    }

    DataGridViewRow currentRow = this.DataGridView_Users.CurrentRow;

    User user = currentRow.DataBoundItem as User;

    if (user == null)
    {
        return;
    }

    ChildForm form = new ChildForm(user);
    form.Owner = this;
    form.ShowDialog();
}

public void LoadLatestUsers()
{
    this.DataGridView_Users.DataSource = Repository.GetUsers();
}

子表單
public ChildForm(User user)
    : this()
{
    this._user = user;
}

private void ChildForm_Load(object sender, EventArgs e)
{
    this.TextBox_FirstName.Text = this._user.FirstName;
    this.TextBox_LastName.Text = this._user.LastName;
    this.TextBox_PhoneNumber.Text = this._user.PhoneNumber;
}

private void Button_Update_Click(object sender, EventArgs e)
{
    this._user.FirstName = this.TextBox_FirstName.Text;
    this._user.LastName = this.TextBox_LastName.Text;
    this._user.PhoneNumber = this.TextBox_PhoneNumber.Text;
    Repository.Update(this._user);

    MainForm mainForm = this.Owner as MainForm;
    mainForm.LoadLatestUsers();
    this.Close();
}

此一方法主要是在主表單建立一個公開方法LoadLatestUsers,在開啟子表單時,將子表單的Owner設為主表單。如此在子表單更新完資料後,取得Owner轉為主表單型別並呼叫LoadLatestUsers。以此方式實作,主要的缺點就是子表單與主表單耦合度太高,子表單需要明確知道它的Owner是誰。如果它的Owner需要有兩個表單以上該如何處理?

各位也可以發現,上述的需求就如同一般所說的Publisher-SubscriberObserver Pattern。在.NET Framework 4.0開始,內建了兩個介面IObserver<T>IObservable<T>可供開發人員實作Observer Pattern以達到前述的需求。

子表單
public partial class ChildForm : Form, IObservable<User>
{
    private User _user;
    IObserver<User> _observer;

    // 省略

    private void Button_Update_Click(object sender, EventArgs e)
    {
        this._user.FirstName = this.TextBox_FirstName.Text;
        this._user.LastName = this.TextBox_LastName.Text;
        this._user.PhoneNumber = this.TextBox_PhoneNumber.Text;
        Repository.Update(this._user);

        if (this._observer != null)
        {
            this._observer.OnNext(this._user);
        }

        this.Close();
    }

    #region IObservable<User> Members

    public IDisposable Subscribe(IObserver<User> observer)
    {
        this._observer = observer;
        return null;
    }

    #endregion
}

子表單實作了IObservable<User>介面,上面的範例是假設Subscriber只有一個,如果有多個Subscriber可以自行修改,例如將_observer變數改為Dictionary或List型別。在會員資料更新後,呼叫IObserver<T>介面的OnNext方法以通知Subscriber(s)有更新。

主表單
public partial class MainForm : Form, IObserver<User>
{
    // 省略

    private void DataGridView_Users_CellMouseDoubleClick(object sender, DataGridViewCellMouseEventArgs e)
    {
        // 省略

        ChildForm form = new ChildForm(user);
        form.Subscribe(this);
        form.ShowDialog();
    }

    private void LoadLatestUsers()
    {
        this.DataGridView_Users.DataSource = Repository.GetUsers();
    }

    #region IObserver<User> Members

    public void OnCompleted()
    {
        throw new NotImplementedException();
    }

    public void OnError(Exception error)
    {
        throw new NotImplementedException();
    }

    public void OnNext(User value)
    {
        this.LoadLatestUsers();
    }

    #endregion
}

主表單實作了IObserver<User>介面,並在開啟子表單時,呼叫其Subscribe方法訂閱更新。當子表單更新資料後便會呼叫OnNext方法重新載入最新的會員資料。各位可以看到原本的LoadLatestUser方法已變更為私有方法。OnNext方法傳入的是單一會員資料,如果不想重新載入所有的會員資料,也可以直接更新DataGridView上該筆會員資料以免載入不必要的資料。

以.NET Framework提供的內建介面來實作Observer Pattern,不僅讓子表單與主表單的耦合度降低,若有多個Subscriber,以此方式實作也相當簡單,而子表單也不需要知道它的Subscriber有誰,只管發送更新通知即可,Subscriber自然會作相對應的處理,只要它們都有實作IObserver<User>介面。

完整範例程式碼可至https://github.com/petekcchen/blog/tree/master/DataGridViewObserver下載。

No comments: