簡體   English   中英

在MVVM應用程序中存儲應用程序設置/狀態的位置

[英]Where to store application settings/state in a MVVM application

我是第一次嘗試使用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的主要原因之一是能夠在沒有主機應用程序的情況下測試模型。 具體來說,我對洞察集中的應用程序設置如何影響可測試性以及模擬依賴模型的能力感興趣。

我通常對有一個視圖模型與另一個視圖模型直接通信的代碼感覺不好。 我喜歡這樣的想法,即模式的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,我在這個問題的答案中討論過

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

public class ServiceProvider : IServiceProvider
{
    ...
}

public class ConnectionViewModel 
{
    private IServiceProvider serviceProvider;

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

    ...       
}

然后,您可以使用模擬框架來引入模擬的IServiceProvider,您可以檢查它以確保使用預期參數調用connect方法。

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

當然,一旦你使用EventAggregator來破壞耦合和IOC來促進測試,它可能值得研究一個依賴注入框架,比如微軟的Prism,但是即使你在開發過程中來不及重新構建很多規則和模式可以以更簡單的方式應用於現有代碼。

如果您沒有使用MV-VM,解決方案很簡單:您將此數據和功能放在Application派生類型中。 Application.Current然后允許您訪問它。 正如您所知,這里的問題是Application.Current在單元測試ViewModel時會導致問題。 這是需要修復的。 第一步是將自己與具體的Application實例分離。 通過定義接口並在具體的Application類型上實現它來完成此操作。

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

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

現在,下一步是使用Inversion of Control或Service Locator消除ViewModel中對Application.Current的調用。

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

  //...
}

現在,所有“全局”功能都通過可模擬的服務接口IApplication提供。 您仍然需要如何使用正確的服務實例構建ViewModel,但聽起來您已經在處理它了嗎? 如果您正在尋找解決方案,Onyx(免責聲明,我是作者)可以在那里提供解決方案。 您的應用程序將訂閱View.Created事件並將其自身添加為服務,框架將處理其余事件。

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

在Prism 2中,它們的區域有點像“數據總線”。 一個控件可能會生成帶有添加到總線的密鑰的數據,而任何想要該數據的控件都可以在該數據發生變化時注冊回調。

就個人而言,我實現了一些我稱之為“ApplicationState”的東西。 它有同樣的目的。 它實現了INotifyPropertyChanged,系統中的任何人都可以寫入特定屬性或訂閱更改事件。 它不像Prism解決方案那么通用,但它有效。 這幾乎就是你創造的。

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

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

樹中的任何可視組件都可以實現此接口,並簡單地將Application狀態傳遞給ViewModel。

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

另一方面,Prism解決了所有這些問題。 我希望我可以回去使用Prism重新設計......但是對我來說,要有成本效益還為時已晚。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM