在MVVM应用程序中,应该将应用程序设置/状态存储在哪里?

27
我第一次尝试使用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个回答

14

我通常对直接通信的一个视图模型与另一个视图模型的代码感到不好的预感。我喜欢这个模式中的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中传递它,但我不太喜欢这种方式,因为它增加了视图模型的耦合,并且视图模型知道需要持久化并不是它的责任。另一个选择是从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,以便检查它是否被调用并传递了预期的参数。

测试另外两个类更具挑战性,因为它们将依赖于真实的服务器和真实的持久化存储设备。您可以添加更多间接层来延迟这一点(例如,SettingsProvider使用的PersistenceProvider),但最终你会进入集成测试的领域。通常情况下,在我使用上述模式进行编码时,模型和视图模型可以获得很好的单元测试覆盖率,但提供者需要更复杂的测试方法。

当然,一旦您使用EventAggregator打破耦合,使用IOC促进测试,可能值得考虑依赖注入框架之一,例如Microsoft's Prism,但即使在开发过程中太晚改变架构,许多规则和模式仍然可以以更简单的方式应用于现有代码。


10
如果您没有使用M-V-VM,解决方案很简单:将这些数据和功能放在派生自Application的类型中。然后可以通过Application.Current访问它。问题在于,像您所知道的那样,在对ViewModel进行单元测试时,Application.Current会导致问题。需要解决这个问题。第一步是将自己与具体的Application实例解耦。通过定义接口并在具体的Application类型上实现它来完成此操作。
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事件并将自己添加为服务,框架将处理其余部分。


我最近几天一直在研究Onyx代码,以便更好地了解WPF。它的布局确实符合我的思维方式,我也学到了很多东西。 - Paul Alexander
谢谢。即使您不使用Onyx本身,我希望这些想法对您有用。当然,在这里并不需要Onyx,但我认为服务接口解决方案确实是您正在寻找的。 - wekempf

2

是的,你正在正确的道路上。当你的系统中有两个控件需要通信数据时,你希望以尽可能解耦的方式进行。有几种方法可以做到这一点。

在Prism 2中,他们有一个类似于“数据总线”的区域。一个控件可能会使用添加到总线上的键生成数据,任何想要该数据的控件都可以在该数据更改时注册回调。

就我个人而言,我实现了一些我称之为“应用程序状态”的东西。它具有相同的目的。它实现了INotifyPropertyChanged接口,系统中的任何人都可以写入特定属性或订阅更改事件。它比Prism解决方案不太通用,但它起作用。这基本上就是你创建的内容。

但现在,你面临着如何传递应用程序状态的问题。传统的方法是将其作为单例模式。我不太喜欢这种方法。相反,我定义了一个接口:

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

任何树中的可视组件都可以实现此接口,并将应用程序状态简单地传递给ViewModel。
然后,在根窗口中,当触发Loaded事件时,我遍历可视树并查找需要应用程序状态(IApplicationStateConsumer)的控件。 我将其传递给appState,然后我的系统就初始化了。 这是一种贫民的依赖注入。
另一方面,Prism解决了所有这些问题。 我有点希望能够回头重新设计使用Prism ...但现在为时已晚,成本效益不高。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接