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

Pau*_*der 26 .net c# 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;
    }

    ...
}
Run Code Online (Sandbox Code Playgroud)

连接视图模型:

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; }

    ...
}
Run Code Online (Sandbox Code Playgroud)

所以问题是:ApplicationViewModel是处理这个问题的正确方法吗?你怎么可能存储应用程序状态?

编辑:我也想知道它如何影响可测试性.使用MVVM的主要原因之一是能够在没有主机应用程序的情况下测试模型.具体来说,我对洞察集中的应用程序设置如何影响可测试性以及模拟依赖模型的能力感兴趣.

Mar*_*ris 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);
    }        
}
Run Code Online (Sandbox Code Playgroud)

接下来要决定的是地址如何到达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
   }
}
Run Code Online (Sandbox Code Playgroud)

这引入了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;
    }

    ...       
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以使用模拟框架来引入模拟的IServiceProvider,您可以检查它以确保使用预期参数调用connect方法.

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

当然,一旦你使用EventAggregator来破坏耦合和IOC来促进测试,它可能值得研究一个依赖注入框架,比如微软的Prism,但是即使你在开发过程中来不及重新构建很多规则和模式可以以更简单的方式应用于现有代码.


wek*_*mpf 9

如果您没有使用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
}
Run Code Online (Sandbox Code Playgroud)

现在,下一步是使用Inversion of Control或Service Locator消除ViewModel中对Application.Current的调用.

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

  //...
}
Run Code Online (Sandbox Code Playgroud)

现在,所有"全局"功能都通过可模拟的服务接口IApplication提供.您仍然需要如何使用正确的服务实例构建ViewModel,但听起来您已经在处理它了吗?如果您正在寻找解决方案,Onyx(免责声明,我是作者)可以在那里提供解决方案.您的应用程序将订阅View.Created事件并将其自身添加为服务,框架将处理其余事件.