在MVVM应用程序中存储应用程序设置/状态的地方

问题描述:

我正在第一次尝试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的主要原因之一是无需主机应用程序即可测试模型。具体而言,我对如何集中应用程序设置影响可测试性以及嘲笑依赖模型的能力有所了解。

如果您未使用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中,它们有一个类似于“数据总线”的区域。一个控件可以通过添加到总线上的键生成数据,并且任何希望该数据的控件都可以在数据更改时注册回调。

就我个人而言,我实现了一些我称之为“ApplicationState”的东西。它有相同的目的。它实现INotifyPropertyChanged,系统中的任何人都可以写入特定的属性或订阅更改事件。它不如Prism解决方案通用,但它的工作原理。这几乎是你创造的。

但是现在,您有如何传递应用程序状态的问题。老派的做法是让它成为一个单身人士。我不是这个的忠实粉丝。相反,我的接口定义为:

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

树中的任何视觉组件可以实现这个接口,并简单地通过应用程序状态的视图模型。

然后,在根窗口中,当Loaded事件触发时,我遍历可视化树并查找需要应用程序状态(IApplicationStateConsumer)的控件。我把他们的appState,我的系统初始化。这是一个穷人的依赖注入。

另一方面,棱镜解决了所有这些问题。我有点希望我可以回去重新使用棱镜设计师......但是对我来说有点太划算了。

我通常对代码有一种不好的感觉,那就是有一个视图模型直接与另一个视图模型进行通信。我喜欢这种模式的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便于测试它可能是值得探讨的依赖注入框架,如微软的棱镜之一,但即使你是来不及沿发展重新 - 构建许多规则和模式可以以更简单的方式应用于现有代码。