Where to store application settings/state in a MVVM application

前端 未结 3 432
囚心锁ツ
囚心锁ツ 2021-01-30 11:18

I\'m experimenting with MVVM for the first time and really like the separation of responsibilities. Of course any design pattern only solves many problems - not all. So I\'m try

相关标签:
3条回答
  • 2021-01-30 11:59

    Yes, you are on the right track. When you have two controls in your system that need to communicate data, you want to do it in a way that is as decoupled as possible. There are several ways to do this.

    In Prism 2, they have an area that is kind of like a "data bus". One control might produce data with a key that is added to the bus, and any control that wants that data can register a callback when that data changes.

    Personally, I have implemented something I call "ApplicationState". It has the same purpose. It implements INotifyPropertyChanged, and anyone in the system can write to the specific properties or subscribe for change events. It is less generic than the Prism solution, but it works. This is pretty much what you created.

    But now, you have the problem of how to pass around the application state. The old school way to do this is to make it a Singleton. I am not a big fan of this. Instead, I have an interface defined as:

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

    Any visual component in the tree may implement this interface, and simply pass the Application state to the ViewModel.

    Then, in the root window, when the Loaded event is fired, I traverse the visual tree and look for controls that want the app state (IApplicationStateConsumer). I hand them the appState, and my system is initialized. It is a poor-man's dependency injection.

    On the other hand, Prism solves all of these problems. I kind of wish I could go back and re-architect using Prism... but it is a bit too late for me to be cost-effective.

    0 讨论(0)
  • 2021-01-30 12:05

    If you weren't using M-V-VM, the solution is simple: you put this data and functionality in your Application derived type. Application.Current then gives you access to it. The problem here, as you're aware, is that Application.Current causes problems when unit testing the ViewModel. That's what needs to be fixed. The first step is to decouple ourselves from a concrete Application instance. Do this by defining an interface and implementing it on your concrete Application type.

    public interface IApplication
    {
      Uri Address{ get; set; }
      void ConnectTo(Uri address);
    }
    
    public class App : Application, IApplication
    {
      // code removed for brevity
    }
    

    Now the next step is to eliminate the call to Application.Current within the ViewModel by using Inversion of Control or Service Locator.

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

    All of the "global" functionality is now provided through a mockable service interface, IApplication. You're still left with how to construct the ViewModel with the correct service instance, but it sounds like you're already handling that? If you're looking for a solution there, Onyx (disclaimer, I'm the author) can provide a solution there. Your Application would subscribe to the View.Created event and add itself as a service and the framework would deal with the rest.

    0 讨论(0)
  • 2021-01-30 12:09

    I generally get a bad feeling about code that has one view model directly communicating with another. I like the idea that the VVM part of the pattern should be basically pluggable and nothing inside that area of the code should depend of the existence of anything else within that section. The reasoning behind this is that without centralising the logic it can become difficult to define responsibility.

    On the other hand, based on your actual code, it may just be that the ApplicationViewModel is badly named, it doesn't make a model accessible to a view, so this may simply be a poor choice of name.

    Either way, the solution comes down to a break down of responsibility. The way I see it you have three things to achieve:

    1. Allow the user to request to connect to an address
    2. Use that address to connect to a server
    3. Persist that address.

    I'd suggest that you need three classes instead of your two.

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

    The next thing to decide is how the address gets to the SettingsProvider. You could pass it in from the ConnectionViewModel as you do currently, but I'm not keen on that because it increases the coupling of the view model and it isn't the responsibility of the ViewModel to know that it needs persisting. Another option is to make the call from the ServiceProvider, but it doesn't really feel to me like it should be the ServiceProvider's responsibility either. In fact it doesn't feel like anyone's responsibility other than the SettingsProvider. Which leads me to believe that the setting provider should listen out for changes to the connected address and persist them without intervention. In other words an event:

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

    This introduces tight coupling between the ServiceProvider and the SettingsProvider, which you want to avoid if possible and I'd use an EventAggregator here, which I've discussed in an answer to this question

    To address the issues of testability, you now have a very defined expectancy for what each method will do. The ConnectionViewModel will call connect, The ServiceProvider will connect and the SettingsProvider will persist. To test the ConnectionViewModel you probably want to convert the coupling to the ServiceProvider from a class to an interface:

    public class ServiceProvider : IServiceProvider
    {
        ...
    }
    
    public class ConnectionViewModel 
    {
        private IServiceProvider serviceProvider;
    
        public ConnectionViewModel(IServiceProvider provider)
        {
            this.serviceProvider = serviceProvider;
        }
    
        ...       
    }
    

    Then you can use a mocking framework to introduce a mocked IServiceProvider that you can check to ensure that the connect method was called with the expected parameters.

    Testing the other two classes is more challenging since they will rely on having a real server and real persistent storage device. You can add more layers of indirection to delay this (for example a PersistenceProvider that the SettingsProvider uses) but eventually you leave the world of unit testing and enter integration testing. Generally when I code with the patterns above the models and view models can get good unit test coverage, but the providers require more complicated testing methodologies.

    Of course, once you are using a EventAggregator to break coupling and IOC to facilitate testing it is probably worth looking into one of the dependency injection frameworks such as Microsoft's Prism, but even if you are too late along in development to re-architect a lot of the rules and patterns can be applied to existing code in a simpler way.

    0 讨论(0)
提交回复
热议问题