Winforms - MVP Pattern: Using static ApplicationController to coordinate application?

后端 未结 1 1366
广开言路
广开言路 2021-01-05 23:11

Background

I\'m building a two-tiered C# .net application:

  1. Tier 1: Winforms client application using the MVP (Model-View-Presenter) de
1条回答
  •  囚心锁ツ
    2021-01-05 23:54

    A static class is of course handy in some cases but there are a lot of downsides to this approach.

    • The tend to grow into something like a God class. You already see this happening. So this class violates SRP
    • A static class cannot have dependencies and therefore it needs to use the Service Locator anti pattern to get it's dependencies. This is not a problem perse if you consider this class to be part of the composition root, but nevertheless, this often heads the wrong way.

    In the supplied code I see three responsibilities of this class.

    1. EventAggregator
    2. What you call Session information
    3. A service to open other views

    Some feedback on this three parts:

    EventAggregator

    Although this is a widely used pattern and sometimes it can be very powerful I myself am not fond of this pattern. I see this pattern as something that provides optional runtime data where in most cases this runtime data is not optional at all. In other words, only use this pattern for truly optional data. For everything that is not really optional, use hard dependencies, using constructor injection.

    The ones that need the information in that case depend upon IEventListener. The one that publish the event, depend upon IEventPublisher.

    public interface IEventListener 
    {
        event Action MessageReceived;
    }
    
    public interface IEventPublisher 
    {
        void Publish(TMessage message);
    }
    
    public class EventPublisher : IEventPublisher 
    {
        private readonly EventOrchestrator orchestrator;
    
        public EventPublisher(EventOrchestrator orchestrator) 
        {
            this.orchestrator = orchestrator;
        }
    
        public void Publish(TMessage message) => this.orchestrator.Publish(message);
    }
    
    public class EventListener : IEventListener 
    {
        private readonly EventOrchestrator orchestrator;
    
        public EventListener(EventOrchestrator orchestrator) 
        {
            this.orchestrator = orchestrator;
        }
    
        public event Action MessageReceived 
        {
            add { orchestrator.MessageReceived += value; }
            remove { orchestrator.MessageReceived -= value; }
        }
    }
    
    public class EventOrchestrator 
    {
        public void Publish(TMessage message) => this.MessageReceived(message);
        public event Action MessageReceived = (e) => { };
    }
    

    To be able to guarantee events are stored in one single location, we extract that storage (the event) into its own class, the EventOrchestrator.

    The registration is as follows:

    container.RegisterSingleton(typeof(IEventListener<>), typeof(EventListener<>));
    container.RegisterSingleton(typeof(IEventPublisher<>), typeof(EventPublisher<>));
    container.RegisterSingleton(typeof(EventOrchestrator<>), typeof(EventOrchestrator<>));
    

    Usage is trivial:

    public class SomeView
    {
        private readonly IEventPublisher eventPublisher;
    
        public SomeView(IEventPublisher eventPublisher)
        {
            this.eventPublisher = eventPublisher;
        }
    
        public void GuardSelectionClick(Guard guard)
        {
            this.eventPublisher.Publish(new GuardChanged(guard));
        }
        // other code..
    }
    
    public class SomeOtherView
    {
        public SomeOtherView(IEventListener eventListener)
        {
            eventListener.MessageReceived += this.GuardChanged;
        }
    
        private void GuardChanged(GuardChanged changedGuard)
        {
            this.CurrentGuard = changedGuard.SelectedGuard;
        }
        // other code..
    }
    

    If another view will receive a lot of events you could always wrap all IEventListeners of that View in a specific EventHandlerForViewX class which get all important IEventListener<> injected.

    Session

    In the question you define several ambient context variables as Session information. Exposing this kind of information through a static class promotes tight coupling to this static class and thus makes it more difficult to unit test parts of your application. IMO all information provided by Session is static (in the sense that it doesn't change throughout the lifetime of the application) data that could just as easily be injected into those parts that actually need this data. So Session should completely be removed from the static class. Some examples how to solve this in a SOLID manner:

    Configuration values

    The composition root is in charge of reading all information from the configuration source (e.g. your app.config file). This information can there be stored in a POCO class crafted for its usage.

    public interface IMailSettings
    {
        string MailAddress { get; }
        string DefaultMailSubject { get; }
    }
    
    public interface IFtpInformation
    {
        int FtpPort { get; }
    }
    
    public interface IFlowerServiceInformation
    {
        string FlowerShopAddress { get; }
    }
    
    public class ConfigValues :
        IMailSettings, IFtpInformation, IFlowerServiceInformation
    {
        public string MailAddress { get; set; }
        public string DefaultMailSubject { get; set; }
    
        public int FtpPort { get; set; }
    
        public string FlowerShopAddress { get; set; }
    }
    // Register as
    public static void RegisterConfig(this Container container)
    {
        var config = new ConfigValues
        {
            MailAddress = ConfigurationManager.AppSettings["MailAddress"],
            DefaultMailSubject = ConfigurationManager.AppSettings["DefaultMailSubject"],
            FtpPort = Convert.ToInt32(ConfigurationManager.AppSettings["FtpPort"]),
            FlowerShopAddress = ConfigurationManager.AppSettings["FlowerShopAddress"],
        };
    
        var registration = Lifestyle.Singleton.CreateRegistration(() => 
                                    config, container);
        container.AddRegistration(typeof(IMailSettings),registration);
        container.AddRegistration(typeof(IFtpInformation),registration);
        container.AddRegistration(typeof(IFlowerServiceInformation),registration);
    }
    

    And where you need some specific information, e.g. information to send an email you can just put IMailSettings in the constructor of the type needing the information.

    This will also give you the possibility to test a component using different config values, which would be harder to do if all config information had to come from the static ApplicationController.

    For security information, e.g. the logged on User the same pattern can be used. Define an IUserContext abstraction, create a WindowsUserContext implementation and fill this with the logged on user in the composition root. Because the component now depends on IUserContext instead of getting the user at runtime from the static class, the same component could also be used in an MVC application, where you would replace the WindowsUserContext with an HttpUserContext implementation.

    Opening other forms

    This is actually the hard part. I normally also use some big static class with all kinds of methods to open other forms. I don't expose the IFormOpener from this answer to my other forms, because they only need to know, what to do, not which form does that task for them. So my static class exposes this kinds of methods:

    public SomeReturnValue OpenCustomerForEdit(Customer customer)
    { 
         var form = MyStaticClass.FormOpener.GetForm();
         form.SetCustomer(customer);
         var result = MyStaticClass.FormOpener.ShowModalForm(form);
         return (SomeReturnValue) result;
    }
    

    However....

    I'm not at all happy with this approach, because over time this class grows and grows. With WPF I use another mechanism, which I think could also be used with WinForms. This approach is based on a message based architecture described in this and this awesome blogposts. Although at first the information looks as it is not at all related, it is the message based concept that let these patterns rock!

    All my WPF windows implement an open generic interface, e.g. IEditView. And if some view needs to edit a customer, it just get's this IEditView injected. A decorator is used to actually show the view in pretty much the same way as the forementioned FormOpener does it. In this case I make use of a specific Simple Injector feature, called decorate factory decorator, which you can use to create forms whenever it is needed, just as the FormOpener used the container directly to create forms whenever it needs to.

    So I did not really test this, so there could be some pitfalls with WinForms, but this code seems to work on a first and single run..

    public class EditViewShowerDecorator : IEditView
    {
        private readonly Func> viewCreator;
    
        public EditViewShowerDecorator(Func> viewCreator)
        {
            this.viewCreator = viewCreator;
        }
    
        public void EditEntity(TEntity entity)
        {
            // get view from container
            var view = this.viewCreator.Invoke();
            // initview with information
            view.EditEntity(entity);
            using (var form = (Form)view)
            {
                // show the view
                form.ShowDialog();
            }
        }
    }
    

    The forms and decorator should be registered as:

    container.Register(typeof(IEditView<>), new[] { Assembly.GetExecutingAssembly() });
    container.RegisterDecorator(typeof(IEditView<>), typeof(EditViewShowerDecorator<>), 
                                 Lifestyle.Singleton);
    

    Security

    The IUserContext must the base for all security.

    For the userinterface I normally hide all controls/buttons that a certain userrole doesn't have access to. The best place is to perform this in the Load event.

    Because I use the command/handler pattern as described here for my all actions external of my forms/views I use a decorator to check if a user has permission to perform this certain command (or query).

    I would advise you to read this post a few times until you really get the hang of it. Once you get familiar with this pattern you won't do anything else!

    If you have any questions about these patterns and how to apply a (permission)decorator, add a comment!

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