【问题标题】:Where to store application settings/state in a MVVM application在 MVVM 应用程序中存储应用程序设置/状态的位置
【发布时间】:2010-10-21 12:01:11
【问题描述】:

我是第一次尝试使用 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 的主要原因之一是能够在没有主机应用程序的情况下测试模型。具体来说,我有兴趣深入了解集中式应用设置如何影响可测试性以及模拟依赖模型的能力。

【问题讨论】:

    标签: c# .net wpf unit-testing mvvm


    【解决方案1】:

    我通常对具有一个视图模型直接与另一个视图模型通信的代码有一种不好的感觉。我喜欢这样的想法,即模式的 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,您可以检查它以确保使用预期的参数调用了 connect 方法。

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

    当然,一旦您使用 EventAggregator 来打破耦合并使用 IOC 来促进测试,那么可能值得研究其中一个依赖注入框架,例如 Microsoft 的 Prism,但即使您在开发过程中为时已晚,无法重新-架构师的许多规则和模式可以以更简单的方式应用于现有代码。

    【讨论】:

      【解决方案2】:

      如果您没有使用 M-V-VM,解决方案很简单:您将这些数据和功能放入您的应用程序派生类型中。 Application.Current 然后让您访问它。如您所知,这里的问题是 Application.Current 在对 ViewModel 进行单元测试时会导致问题。这就是需要解决的问题。第一步是将自己与具体的应用程序实例解耦。通过定义一个接口并在具体的应用程序类型上实现它来做到这一点。

      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 的一些见解。它肯定阐述了我的想法,我学到了很多。
      • 谢谢。即使您不使用 Onyx 本身,我也希望这些想法有用。这里当然不需要 Onyx,尽管我认为服务接口解决方案确实是您正在寻找的。​​span>
      【解决方案3】:

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

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

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

      但是现在,您遇到了如何传递应用程序状态的问题。执行此操作的老派方法是使其成为单例。我不是这个的忠实粉丝。相反,我有一个接口定义为:

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

      树中的任何可视化组件都可以实现这个接口,只需将应用程序状态传递给 ViewModel。

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

      另一方面,Prism 解决了所有这些问题。我有点希望我可以回去使用 Prism 重新架构...但是对我来说成本效益有点太晚了。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-02-26
        • 2011-01-14
        • 1970-01-01
        • 2022-01-21
        • 1970-01-01
        • 2015-11-24
        相关资源
        最近更新 更多