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

时间:2009-04-23 23:13:05

标签: c# .net wpf unit-testing 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的主要原因之一是能够在没有主机应用程序的情况下测试模型。具体来说,我对洞察集中应用程序设置如何影响可测试性以及模拟依赖模型的能力感兴趣。

3 个答案:

答案 0 :(得分:11)

对于有一个视图模型直接与另一个视图模型通信的代码,我通常会感觉不好。我喜欢这样的想法,即模式的VVM部分应该基本上是可插拔的,并且代码区域内的任何内容都不应该依赖于该部分中其他任何内容的存在。这背后的原因是,如果不集中逻辑,就很难确定责任。

另一方面,根据您的实际代码,可能只是ApplicationViewModel命名错误,它不能使视图访问模型,因此这可能只是名称的不良选择。

无论哪种方式,解决方案都归结为责任的分解。我认为你有三件事要做:

  1. 允许用户请求连接到地址
  2. 使用该地址连接服务器
  3. 坚持该地址。
  4. 我建议您需要三个班级而不是两个班级。

    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,你可以检查它以确保使用期望的参数调用connect方法。

    测试其他两个类更具挑战性,因为它们将依赖于真正的服务器和真正的持久存储设备。您可以添加更多层间接来延迟这一点(例如,SettingsProvider使用的PersistenceProvider),但最终您将离开单元测试的世界并进入集成测试。通常,当我使用上面的模型进行编码时,模型和视图模型可以获得良好的单元测试覆盖率,但是提供者需要更复杂的测试方法。

    当然,一旦你使用EventAggregator来破坏耦合和IOC来促进测试,它可能值得研究一个依赖注入框架,如微软的Prism,但即使你在开发过程中为时已晚架构师可以以更简单的方式将许多规则和模式应用于现有代码。

答案 1 :(得分:9)

如果您没有使用M-V-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事件并将其自身添加为服务,框架将处理其余事件。

答案 2 :(得分:2)

是的,你走在正确的轨道上。当系统中有两个需要传递数据的控件时,您希望以尽可能分离的方式执行此操作。有几种方法可以做到这一点。

在棱镜2中,它们的区域有点像“数据总线”。一个控件可能会生成带有添加到总线的键的数据,而任何想要该数据的控件都可以在数据发生变化时注册回调。

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

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

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

树中的任何可视组件都可以实现此接口,只需将Application状态传递给ViewModel即可。

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

另一方面,Prism解决了所有这些问题。我希望我可以回去使用Prism重新设计......但是对我来说,要有成本效益还为时已晚。