Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/csharp/281.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# 在MVVM应用程序中存储应用程序设置/状态的位置_C#_.net_Wpf_Unit Testing_Mvvm - Fatal编程技术网

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

C# 在MVVM应用程序中存储应用程序设置/状态的位置,c#,.net,wpf,unit-testing,mvvm,C#,.net,Wpf,Unit Testing,Mvvm,我第一次尝试使用MVVM,非常喜欢职责分离。当然,任何设计模式都只能解决很多问题,而不是全部问题。所以我试图找出在哪里存储应用程序状态以及在哪里存储应用程序范围的命令 假设我的应用程序连接到一个特定的URL。我有一个ConnectionWindow和一个ConnectionViewModel,它们支持从用户收集这些信息并调用命令来连接地址。下次应用程序启动时,我希望在不提示用户的情况下重新连接到此地址 到目前为止,我的解决方案是创建一个ApplicationViewModel,它提供一个命令来连

我第一次尝试使用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; }

    ...
}
连接视图模型:

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

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

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

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

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

public interface IApplicationStateConsumer
{
    public void ConsumeApplicationState(ApplicationState appState);
}
树中的任何可视组件都可以实现此接口,并将应用程序状态传递给ViewModel

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


另一方面,棱镜解决了所有这些问题。我有点希望我可以回去重新设计使用棱镜。。。但是,对于我来说,要实现成本效益已经太晚了。

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

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

另一方面,根据您的实际代码,可能只是ApplicationViewModel的名称不正确,它无法使模型对视图进行访问,因此这可能只是一个糟糕的名称选择

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

  • 允许用户请求连接到某个地址
  • 使用该地址连接到服务器
  • 坚持那个地址
  • 我建议你需要三节课而不是两节课

    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没有责任知道它需要持久化。另一种选择是从服务提供商那里打电话,但我觉得这也不是服务提供商的责任。事实上,除了设置提供者之外,任何人都没有责任。这让我相信设置提供者应该监听对连接地址的更改,并在不进行干预的情况下保持这些更改。换句话说,事件:

    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
       }
    }
    
    然后,您可以使用模拟框架引入一个模拟的IServiceProvider,您可以检查它以确保使用预期参数调用了connect方法

    试验