2009-04-23 143 views
26

我正在第一次嘗試MVVM,並且非常喜歡分離職責。當然,任何設計模式都只能解決許多問題 - 並非全部。所以我試圖找出存儲應用程序狀態的位置以及存儲應用程序範圍命令的位置。在MVVM應用程序中存儲應用程序設置/狀態的地方

比方說我的應用程序連接到一個特定的URL。我有一個ConnectionWindow和一個ConnectionViewModel,支持從用戶收集這些信息並調用命令來連接到該地址。下一次應用程序啓動時,我想在不提示用戶的情況下重新連接到該地址。

我到目前爲止的解決方案是創建一個ApplicationViewModel,它提供了一個連接到特定地址的命令,並將該地址保存到某個持久性存儲器(實際保存的地址與此問題無關)。下面是一個縮寫的類模型。

應用視圖模型:

public class ApplicationViewModel : INotifyPropertyChanged 
{ 
    public Uri Address{ get; set; } 
    public void ConnectTo(Uri address) 
    { 
     // Connect to the address 
     // Save the addres in persistent storage for later re-use 
     Address = address; 
    } 

    ... 
} 

連接視圖模型:

public class ConnectionViewModel : INotifyPropertyChanged 
{ 
    private ApplicationViewModel _appModel; 
    public ConnectionViewModel(ApplicationViewModel model) 
    { 
     _appModel = model; 
    } 

    public ICommand ConnectCmd 
    { 
     get 
     { 
      if(_connectCmd == null) 
      { 
       _connectCmd = new LambdaCommand(
        p => _appModel.ConnectTo(Address), 
        p => Address != null 
        ); 
      } 
      return _connectCmd; 
     } 
    }  

    public Uri Address{ get; set; } 

    ... 
} 

所以,問題是這樣的:是的ApplicationViewModel來處理這個正確的方式?你還可以存儲應用程序狀態嗎?

編輯:我也想知道這是如何影響可測試性的。使用MVVM的主要原因之一是無需主機應用程序即可測試模型。具體而言,我對如何集中應用程序設置影響可測試性以及嘲笑依賴模型的能力有所瞭解。

回答

10

如果您未使用M-V-VM,則解決方案很簡單:您將此數據和功能置於Application派生類型中。 Application.Current然後讓你訪問它。正如你所知道的,這裏的問題是,當單元測試ViewModel時,Application.Current會導致問題。這就是需要解決的問題。第一步是將自己與具體的應用程序實例分開。通過定義一個接口並在具體應用程序類型上實現它來實現這一點。

public interface IApplication 
{ 
    Uri Address{ get; set; } 
    void ConnectTo(Uri address); 
} 

public class App : Application, IApplication 
{ 
    // code removed for brevity 
} 

現在接下來的步驟是通過使用控制或服務定位器的反轉,以消除在ViewModel內調用Application.Current。

public class ConnectionViewModel : INotifyPropertyChanged 
{ 
    public ConnectionViewModel(IApplication application) 
    { 
    //... 
    } 

    //... 
} 

所有「全局」功能現在都通過可馴服的服務接口IApplication提供。你仍然留下如何構建具有正確服務實例的ViewModel,但聽起來你已經在處理它了?如果您在那裏尋找解決方案,Onyx(免責聲明,我是作者)可以在那裏提供解決方案。您的應用程序將訂閱View.Created事件並將其自身添加爲一項服務,該框架將處理剩下的事件。

+0

我已經在過去的幾天中傾注了Onyx代碼來收集WPF的一些見解。這絕對是我思考的方式,我學到了很多東西。 – 2009-04-28 23:26:30

2

是的,你是在正確的軌道上。當系統中需要傳遞數據的兩個控件時,您希望以儘可能分離的方式進行操作。有幾種方法可以做到這一點。

在棱鏡2中,它們有一個類似於「數據總線」的區域。一個控件可以通過添加到總線上的鍵生成數據,並且任何希望該數據的控件都可以在數據更改時註冊回調。

就我個人而言,我實現了一些我稱之爲「ApplicationState」的東西。它有相同的目的。它實現INotifyPropertyChanged,系統中的任何人都可以寫入特定的屬性或訂閱更改事件。它不如Prism解決方案通用,但它的工作原理。這幾乎是你創造的。

但是現在,您有如何傳遞應用程序狀態的問題。老派的做法是讓它成爲一個單身人士。我不是這個的忠實粉絲。相反,我的接口定義爲:

public interface IApplicationStateConsumer 
{ 
    public void ConsumeApplicationState(ApplicationState appState); 
} 

樹中的任何視覺組件可以實現這個接口,並簡單地通過應用程序狀態的視圖模型。

然後,在根窗口中,當Loaded事件觸發時,我遍歷可視化樹並查找需要應用程序狀態(IApplicationStateConsumer)的控件。我把他們的appState,我的系統初始化。這是一個窮人的依賴注入。

另一方面,棱鏡解決了所有這些問題。我有點希望我可以回去重新使用棱鏡設計師......但是對我來說有點太划算了。

11

我通常對代碼有一種不好的感覺,那就是有一個視圖模型直接與另一個視圖模型進行通信。我喜歡這種模式的VVM部分應該基本可插入的想法,並且代碼的該區域內的任何內容都不應該取決於該部分中是否存在其他任何內容。這背後的原因是,如果沒有集中邏輯,就很難界定責任。另一方面,根據你的實際代碼,它可能只是ApplicationViewModel命名錯誤,它並沒有使一個模型可以被視圖訪問,所以這可能只是一個很差的名稱選擇。

無論哪種方式,解決方案歸結爲責任分解。我看到它的方式,你有三件事情來實現:

  1. 允許用戶請求連接到地址
  2. 使用該地址連接到服務器
  3. 堅持該地址。

我建議你需要三個類而不是兩個。

public class ServiceProvider 
{ 
    public void Connect(Uri address) 
    { 
     //connect to the server 
    } 
} 

public class SettingsProvider 
{ 
    public void SaveAddress(Uri address) 
    { 
     //Persist address 
    } 

    public Uri LoadAddress() 
    { 
     //Get address from storage 
    } 
} 

public class ConnectionViewModel 
{ 
    private ServiceProvider serviceProvider; 

    public ConnectionViewModel(ServiceProvider provider) 
    { 
     this.serviceProvider = serviceProvider; 
    } 

    public void ExecuteConnectCommand() 
    { 
     serviceProvider.Connect(Address); 
    }   
} 

接下來要決定的是地址到達SettingsProvider的方式。您可以像現在一樣從ConnectionViewModel傳入它,但我並不熱衷於此,因爲它增加了視圖模型的耦合性,ViewModel不負責知道它需要持續存在。另一種選擇是從ServiceProvider進行調用,但它並不真的感覺我應該是ServiceProvider的責任。事實上,除了SettingsProvider以外,其他任何人都沒有責任感。這導致我相信設置提供商應該聽取對連接地址的更改並堅持不用幹預。換句話說事件:

public class ServiceProvider 
{ 
    public event EventHandler<ConnectedEventArgs> Connected; 
    public void Connect(Uri address) 
    { 
     //connect to the server 
     if (Connected != null) 
     { 
      Connected(this, new ConnectedEventArgs(address)); 
     } 
    } 
} 

public class SettingsProvider 
{ 

    public SettingsProvider(ServiceProvider serviceProvider) 
    { 
     serviceProvider.Connected += serviceProvider_Connected; 
    } 

    protected virtual void serviceProvider_Connected(object sender, ConnectedEventArgs e) 
    { 
     SaveAddress(e.Address); 
    } 

    public void SaveAddress(Uri address) 
    { 
     //Persist address 
    } 

    public Uri LoadAddress() 
    { 
     //Get address from storage 
    } 
} 

這裏主要介紹的ServiceProvider和SettingsProvider之間的緊耦合,要儘可能避免和我在這裏使用的EventAggregator,我已經在回答this question討論

爲了解決可測試性的問題,您現在對每種方法的作用有非常確定的期望。 ConnectionViewModel將調用connect,ServiceProvider將連接並且SettingsProvider將持續。爲了測試ConnectionViewModel你可能要耦合從一個類轉換成的ServiceProvider到一個接口:

public class ServiceProvider : IServiceProvider 
{ 
    ... 
} 

public class ConnectionViewModel 
{ 
    private IServiceProvider serviceProvider; 

    public ConnectionViewModel(IServiceProvider provider) 
    { 
     this.serviceProvider = serviceProvider; 
    } 

    ...  
} 

然後你可以使用一個模擬框架引入嘲笑的IServiceProvider,你可以檢查,以確保連接方法被稱爲預期的參數。

測試其他兩個類更具挑戰性,因爲它們將依賴於具有真正的服務器和真正的持久存儲設備。你可以添加更多的間接層以延遲這一點(例如SettingsProvider使用的PersistenceProvider),但最終你會離開單元測試的世界並進入集成測試。通常,當我使用上述模式進行編碼時,模型和視圖模型可以獲得很好的單元測試覆蓋率,但提供者需要更復雜的測試方法。

當然,一旦你使用的是EventAggregator打破耦合和IOC便於測試它可能是值得探討的依賴注入框架,如微軟的棱鏡之一,但即使你是來不及沿發展重新 - 構建許多規則和模式可以以更簡單的方式應用於現有代碼。