Search code examples
c#wpfmvvmmvvm-lightsimple-injector

What is an alternative to resolving instances by key with Simple Injector for a WPF Window mediator service?


Looking at the "Resolve instances by key" section on the Simple Injector website, I've played around with the suggested IRequestHandlerFactory implementations to improve the below code, but the notes such as:

Note: The need for keyed registration can be an indication of ambiguity in the application design and a sign of a Liskov Substitution Principle violation. Take a good look if each keyed registration shouldn’t have its own unique interface, or perhaps each registration should implement its own version of a generic interface.

and

Note: Please remember the previous note about ambiguity in the application design. In the given example the design would probably be better of by using a generic IRequestHandler<TRequest> interface. This would allow the implementations to be batch registered using a single line of code, saves you from using keys, and results in a configuration the is verifiable by the container.

have got me curious.

Q: How would an actual implementation alluded to in the note (IRequestHandler<TRequest>) look for my situation? I've spent a while trying to figure out what it might be but can't come up with a way that works.

What I currently have

To decouple view models, I currently have a view and dialog creator that listens for messages via MVVMLight's Messenger and based on those messages will create the necessary Windows or dialogs requested.

I inject an IWindowResolver which is a factory (although more of a service locator really) that simply gets the requested Window type using the Simple Injector factory pattern on the composition root.

I like the RegisterViewHandler in that it's clear and in one place that it stitches the Message and the associated window and the message handler together rather than being spread-out within the code (eg HandleEmailPopupMessage doesn't need to know about the exact type of window to obtain and the pattern used by HandleEmailPopupMessage can also be made generic for simple window creation and message sending). However, I believe the IWindowResolver could probably be less of a locator and more done to perhaps push some of the registration into the Composition Root.

Secondary Question - Is some kind of IRequestHandler<TRequest> easier/more robust/useful or is simply pushing the Dictionary down to the factory level as the other examples in the Simple Injector doco give, be enough of a tidyup?

// Current IWindowResolver
interface IWindowResolver
{
   Window CreateWindow<TWindow>(TWindow windowType) where TWindow : class;
}

// Current Simple Injector IWindowResolver implementation
[UsedImplicitly]
private sealed class SimpleInjectorWindowFactory : IWindowFactory
{
   private readonly Container _container;

   public SimpleInjectorWindowFactory(Container container)
   {
      _container = container;
   }

   public Window CreateWindow<TWindow>(TWindow windowType) where TWindow : class
   {
      return _container.GetInstance<TWindow>() as Window;
   }
}

public class ShowEmailPopupFormMessage
{
   public ShowEmailPopupFormMessage()
   {
      Params = new ParamsMessage();
   }

   public class ParamsMessage
   {
      public string CustomerName { get; set; }
      public string EmailTo { get; set; }
   }

   public ParamsMessage Params { get; set; }
}

// Current ViewConstructor
class ViewConstructor
{
   IWindowResolver _windowResolver;
   Dictionary<Type, Type> _viewMap = new Dictionary<Type, Type>(); // Maps a message type to a particular window/view type

   public ViewConstructor(IWindowResolver windowResolver)
   {
      _windowResolver = windowResolver;
      RegisterViewHandler<ShowEmailPopupFormMessage, EmailPopupWindow>(HandleEmailPopupMessage);
   }

   private void RegisterViewHandler<TMessage, TWindow>(Action<TMessage> messageAction)
      where TMessage : class
      where TWindow : Window
   {
      if (_viewMap.ContainsKey(typeof(TMessage)))
      {
         throw new ArgumentException("View already registered");
      }

      // Store the map of Message type to Window type
      _viewMap[typeof(TMessage)] = typeof(TWindow);

      // Register with the message handler
      Messenger.Default.Register(this, messageAction);
   }

   private void HandleEmailPopupMessage(ShowEmailPopupFormMessage msg)
   {
      var frm = GetMappedWindow(msg.GetType());

      // We know that the View and it's associated ViewModel are now created
      // so we can send some initialization parameters to the view and or ViewModel
      Messenger.Send(msg.Params);

      frm.ShowDialog();
   }

   private Window GetMappedWindow<TMessage>(TMessage messageType)
   {
      var windowType = _viewMap[typeof(TMessage)];

      var frm = _windowResolver.CreateWindow(windowType);

      if (frm == null)
      {
         throw new ApplicationException("Window is not of the specified Type!");
      }

      // Hookup common events such as cleanup events
      frm.Unloaded += FormOnUnloaded;

      return frm;
   }

   private static void FormOnUnloaded(object sender, RoutedEventArgs eArgs)
   {
      var frm = sender as Window;

      if (frm == null)
      {
         return;
      }

      // Cleanup the ViewModel 
      var dataContext = frm.DataContext as ICleanup;

      if (dataContext != null)
      {
         dataContext.Cleanup();
      }
   }
}

public class EmailPopupWindow : Window
{
   // Window knows how to set it's datacontext's ViewModel (in this case EmailPopupVm) using the ViewModelLocator declared in XML.
   // The window does not handle any messages.
}

// The View Model for the EmailPopupWindow. ViewModelBase is from MVVMLight
public class EmailPopupVm : ViewModelBase
{
   public EmailPopupVm()
   {
      Messenger.Register<ShowEmailPopupFormMessage.ParamsMessage>(HandleParamsMessage);
   }

   private void HandleParamsMessage(ShowEmailPopupFormMessage.ParamsMessage msg)
   {
      // Initialize the ViewModel with the parameters
      this.CustomerName = msg.CustomerName;
      this.EmailTo = msg.EmailTo;
   }
}

Update

For clarity, the ViewModel (now added to the example code above) actually handles the ShowEmailPopupFormMessage.ParamsMessage. The Window is oblivious to any messages.


Solution

  • It is always hard to dive into the details of a specific design and distill something that is actually correct in the context of that application, since many details are usually missing. But I'll give it a try anyway, so my apologies in advance, if I'm a bit off here.

    It seems to me that you are missing an abstraction for classes that can handle messages; let's call it IMessageHandler<TMessage>:

    public interface IMessageHandler<TMessage>
    {
        void Handle(TMessage message);
    }
    

    Next, it seems that windows are also linked to certain messages. So I would define a generic interface for windows well, for instance:

    public interface IWindow<TMessage>
    {
        void ShowDialog();
    }
    

    Next, you'll probably need some abstraction that allows you to send and dispatch messages (although that might already be given to you by MVVMLight, I'm not familiar with that):

    public interface IMessageDispatcher
    {
        void Dispatch(object message);
    }
    

    Based on the IMessageHandler<TMessage> abstraction, we can now create a handler that can handle the ShowEmailPopupFormMessage:

    public class ShowEmailPopupFormMessageHandler : IMessageHandler<ShowEmailPopupFormMessage>
    {
        private readonly IWindow<ShowEmailPopupFormMessage> frm;
        public ShowEmailPopupFormMessageHandler(IWindow<ShowEmailPopupFormMessage> frm) {
            this.frm = frm;
        }
    
        public void Handle(ShowEmailPopupFormMessage message) {
            Messenger.Send(msg.Params);
            frm.ShowDialog();
        }
    }
    

    And snce your EmailPopupWindow handles ShowEmailPopupFormMessage messages, we should let it implement IWindow<ShowEmailPopupFormMessage>:

    public class EmailPopupWindow : Window, IWindow<ShowEmailPopupFormMessage>
    {
        // window stuff here
    }
    

    Now what's left is wiring everything up in the composition root:

    // Composition Root
    container.Register(typeof(IWindow<>), applicationAssemblies);
    container.Register(typeof(IMessageHandler<>), applicationAssemblies);
    container.RegisterSingleton<IMessageDispatcher>(
        new SimpleInjectorMessageDispatcher(container));
    container.RegisterInitializer<Window>(frm => {
        frm.Unloaded += FormOnUnloaded;
    });
    

    Note that both IWindow<T> and IMessageHandler<T> implementations are wired up using batch-registration. Also note that the registration of the Window.Unload event is done in composition root.

    The SimpleInjectorMessageDispatcher is part of the composition root as well:

    private sealed class SimpleInjectorMessageDispatcher : IMessageDispatcher
    {
        private readonly Container container;
        public SimpleInjectorMessageDispatcher(Container container) {
            this.container = container;
        }
    
        public void Dispatch(object message) {
            Type handlerType = typeof(IMessageHandler<>).MakeGenericType(message.GetType());
            dynamic handler = this.container.GetInstance(handlerType);
            handler.Handle((dynamic)message);
        }
    }
    

    Like I said, I might be off a bit (or by a mile), but this would hopefully give you some ideas about how to approach this using generic typing. Generic typing has the advantage that it gives you a consistent design, allows metadata to be burned in the type's definition, simplifies applies cross-cutting concerns (using decorators), and allows easy registration. But the same constraints hold to generic abstractions; they must follow the SOLID principles. So they must be focused and narrow (preferably have just one member on them).