Get Instance using an existing delegate Factory based on Type (or Previous ViewModel)

159 views Asked by At

Based on this page we've created a Wizard that has three steps. Everything works great, but we have one problem with the code given in the link, which is how it creates the next step instance (copy pasted from the link):

protected override IScreen DetermineNextItemToActivate(IList<IScreen> list, int lastIndex)
{
    var theScreenThatJustClosed = list[lastIndex] as BaseViewModel;
    var state = theScreenThatJustClosed.WorkflowState;

    var nextScreenType = TransitionMap.GetNextScreenType(theScreenThatJustClosed);

    var nextScreen = Activator.CreateInstance(nextScreenType, state);

    return nextScreen as IScreen;
}

Currently, it looks like this in our project:

protected override IWizardScreen DetermineNextItemToActivate(IList<IWizardScreen> list, int lastIndex)
{
    var theScreenThatJustClosed = list[lastIndex];
    if (theScreenThatJustClosed == null) throw new Exception("Expecting a screen here!");

    if (theScreenThatJustClosed.NextTransition == WizardTransition.Done)
    {
        TryClose(); // Close the entire Wizard
    }

    var state = theScreenThatJustClosed.WizardAggregateState;
    var nextScreenType = _map.GetNextScreenType(theScreenThatJustClosed);
    if (nextScreenType == null) return null;
    // TODO: CreateInstance requires all constructors for each WizardStep, even if they aren't needed. This should be different!
    var nextScreen = Activator.CreateInstance(nextScreenType, state, _applicationService, _wfdRegisterInstellingLookUp,
        _adresService, _userService, _documentStore, _windowManager, _fileStore, _fileUploadService, _dialogService,
        _eventAggregator, _aanstellingViewModelFactory);

    return nextScreen as IWizardScreen;
}

As you can see, we have quite a few parameters we need in some steps. In step 1 we only need like two, but because of the Activator.CreateInstance(nextScreenType, state, ...); we still need to pass all of them.

What I'd like instead is to use a delegate Factory. We use them at more places in our project, and let AutoFac take care of the rest of the parameters. For each of the three steps we only need a delegate Factory that uses the state.

Because all three uses the same delegate Factory with just state, I've placed this Factory in their Base class:

public delegate WizardBaseViewModel<TViewModel> Factory(AggregateState state);

How I'd like to change the DetermineNextItemToActivate method:

protected override IWizardScreen DetermineNextItemToActivate(IList<IWizardScreen> list, int lastIndex)
{
    var theScreenThatJustClosed = list[lastIndex];
    if (theScreenThatJustClosed == null) throw new Exception("Expecting a screen here!");

    if (theScreenThatJustClosed.NextTransition == WizardTransition.Done)
    {
        TryClose(); // Close the entire Wizard
    }

    return _map.GetNextScreenFactoryInstance(state);
}

But now I'm stuck at making the GetNextScreenFactoryInstance method:

public IWizardScreen GetNextScreenFactoryInstance(IWizardScreen screenThatClosed)
{
    var state = screenThatClosed.WizardAggregateState;

    // This is where I'm stuck. How do I get the instance using the Factory, when I only know the previous ViewModel
    // ** Half-Pseudocode
    var nextType = GetNextScreenType(screenThatClosed);
    var viewModelFactory = get delegate factory based on type?;
    var invokedInstance = viewModelFactory.Invoke(state);
    // **

    return invokedInstance as IWizardScreen;
}

Feel free to change the GetNextScreenFactoryInstance any way you'd like. As long as I can get the next Step-ViewModel based on the previous one in the map.


NOTE: Other relevant code, can be found in the link, but I'll post it here as well to keep it all together:

The WizardTransitionMap (only change is it not being a Singleton anymore, so we can instantiate a map outselves):

public class WizardTransitionMap : Dictionary<Type, Dictionary<WizardTransition, Type>>
{
    public void Add<TIdentity, TResponse>(WizardTransition transition) 
        where TIdentity : IScreen 
        where TResponse : IScreen
    {
        if (!ContainsKey(typeof(TIdentity)))
        {
            Add(typeof(TIdentity), new Dictionary<WizardTransition, Type> { { transition, typeof(TResponse) } });
        }
        else
        {
            this[typeof(TIdentity)].Add(transition, typeof(TResponse));
        }
    }

    public Type GetNextScreenType(IWizardScreen screenThatClosed)
    {
        var identity = screenThatClosed.GetType();
        var transition = screenThatClosed.NextTransition;
        if (!transition.HasValue) return null;

        if (!ContainsKey(identity))
        {
            throw new InvalidOperationException(String.Format("There are no states transitions defined for state {0}", identity));
        }

        if (!this[identity].ContainsKey(transition.Value))
        {
            throw new InvalidOperationException(String.Format("There is no response setup for transition {0} from screen {1}", transition, identity));
        }

        return this[identity][transition.Value];
    }
}

Our InitializeMap-method:

protected override void InitializeMap()
{
    _map = new WizardTransitionMap();

    _map.Add<ScreenOneViewModel, ScreenTwoViewModel>(WizardTransition.Next);

    _map.Add<ScreenTwoViewModel, ScreenOneViewModel>(WizardTransition.Previous);
    _map.Add<ScreenTwoViewModel, ScreenThreeViewModel>(WizardTransition.Next);

    _map.Add<ScreenThreeViewModel, ScreenTwoViewModel>(WizardTransition.Previous);
    _map.Add<ScreenThreeViewModel, ScreenThreeViewModel>(WizardTransition.Done);
}
1

There are 1 answers

0
Kevin Cruijssen On BEST ANSWER

We've changed the code:

The WizardTransitionMap now accepts Delegates. Also, instead of retrieving the type by the WizardTransition-enum value (Next, Previous, etc.), we now retrieve the Factory-invoke based on the next Type (so the inner Dictionary is reversed). So, this is our current WizardTransitionMap:

using System;
using System.Collections.Generic;

namespace NatWa.MidOffice.CustomControls.Wizard
{
    public class WizardTransitionMap : Dictionary<Type, Dictionary<Type, Delegate>>
    {
        public void Add<TCurrentScreenType, TNextScreenType>(Delegate delegateFactory)
        {
            if (!ContainsKey(typeof(TCurrentScreenType)))
            {
                Add(typeof(TCurrentScreenType), new Dictionary<Type, Delegate> { { typeof(TNextScreenType), delegateFactory } });
            }
            else
            {
                this[typeof(TCurrentScreenType)].Add(typeof(TNextScreenType), delegateFactory);
            }
        }

        public IWizardScreen GetNextScreen(IWizardScreen screenThatClosed)
        {
            var identity = screenThatClosed.GetType();
            var state = screenThatClosed.State;
            var transition = screenThatClosed.NextScreenType;

            if (!ContainsKey(identity))
            {
                throw new InvalidOperationException(String.Format("There are no states transitions defined for state {0}", identity));
            }

            if (!this[identity].ContainsKey(transition))
            {
                throw new InvalidOperationException(String.Format("There is no response setup for transition {0} from screen {1}", transition, identity));
            }

            if (this[identity][transition] == null)
                return null;

            return (IWizardScreen)this[identity][transition].DynamicInvoke(state);
        }
    }
}

Our InitializeMap is now changed to this:

protected override void InitializeMap()
{
    _map = new WizardTransitionMap();

    _map.Add<ScreenOneViewModel, ScreenTwoViewModel>(_screenTwoFactory);

    _map.Add<ScreenTwoViewModel, ScreenOneViewModel>(_screenOneFactory);
    _map.Add<ScreenTwoViewModel, ScreenThreeViewModel>(_screenThreeFactory);

    _map.Add<ScreenThreeViewModel, ScreenTwoViewModel>(_screenTwoFactory);
    _map.Add<ScreenThreeViewModel, ScreenThreeViewModel>(null);
}

And our DetemineNexttemToActivate method to this:

protected override IWizardScreen DetermineNextItemToActivate(IList<IWizardScreen> list, int previousIndex)
{
    var theScreenThatJustClosed = list[previousIndex];
    if (theScreenThatJustClosed == null) throw new Exception("Expecting a screen here!");

    var nextScreen = _map.GetNextScreen(theScreenThatJustClosed);

    if (nextScreen == null)
    {
        TryClose();
        return ActiveItem; // Can't return null here, because Caliburn's Conductor will automatically get into this method again with a retry
    }

    return nextScreen;
}

We also removed our entire WizardBaseViewModel and just let every Step-ViewModel implement the IWizardScreen:

public interface IWizardScreen : IScreen
{
    AggregateState State { get; }
    Type NextScreenType { get; }

    void Next();
    void Previous();
}

With the following implementation in our ScreenOneViewModel:

public AggregateState State { get { return _state; }  }
public Type NextScreenType { get; private set; }

public void Next()
{
    if (!IsValid()) return;

    NextScreenType = typeof(ScreenTwoViewModel);
    TryClose();
}

public void Previous()
{
    throw new NotImplementedException(); // Isn't needed in first screen, because we have no previous
}

And the following implementation in our ScreenThreeViewModel:

public AggregateState State { get { return _state; } }
public Type NextScreenType { get; private set; }

public void Next()
{
    NextScreenType = typeof(ScreenThreeViewModel); // Own type, because we have no next
    TryClose();
}

public void Previous()
{
    NextScreenType = typeof(ScreenTwoViewModel);
    TryClose();
}

And each Step-ViewModel has its own delegate Factory, like this one for ScreenTwoViewModel:

public delegate ScreenTwoViewModel Factory(AggregateState state);