Can I implement my own view resolution service and have RequestNavigate use it?
Asked Answered
S

2

5

I 'm fairly new to Prism and I 'm currently re-writing one of our existing applications using Prism as a proof of concept project.

The application uses MVVM with a ViewModel first approach: our ViewModel is resolved by the container, and an IViewResolver service figures out what view it should be wired up to (using name conventions amongst other things).

The code (to add a view to a tab control) at the moment looks something like this:

var vm = (get ViewModel from somewhere)
IRegion reg = _regionManager.Regions["MainRegion"];
var vw = _viewResolver.FromViewModel(vm); // Spins up a view and sets its DataContext
reg.Add(vw);
reg.Activate(vw);

This all works fine, however I 'd really like to use the Prism navigation framework to do all this stuff for me so that I can do something like this:

_regionManager.RequestNavigate(
    "MainRegion", 
    new Uri("NameOfMyViewModel", UriKind.Relative)
);

and have Prism spin up the ViewModel + View, set up the DataContext and insert the view into the region.

I 've had some success by creating DataTemplates referencing the ViewModel types, e.g.:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Module01">
<DataTemplate DataType="{x:Type local:TestViewModel}">
<local:TestView />
</DataTemplate>
</ResourceDictionary>

...and have the module add the relevant resource dictionary into the applications resources when the module is initialized, but that seems a bit rubbish.

Is there a way to effectively take over view creation from Prism, so that when RequestNavigate is called I can look at the supplied Uri and spin up the view / viewmodel based on that? There’s an overload of RegionManager.RegisterViewWithRegion that takes a delegate that allows you to supply a view yourself, and I guess I’m after something like that.

I think I might need to supply my own IRegionBehaviorFactory, but am unsure what's involved (or even if I am on the right path!).

Any help appreciated!

-- note: Originally posted over at the prism codeplex site

Surrey answered 17/9, 2011 at 7:15 Comment(0)
L
8

Sure you can do that. I 've found that Prism v4 is really extensible, if only you know where to plug in.

In this case, you want your own custom implementation of IRegionNavigationContentLoader.

Here's how to set things up in your bootstrapper (the example is from a subclass of UnityBootstrapper from one of my own projects):

protected override void ConfigureContainer()
{
    // IMPORTANT: Due to the inner workings of UnityBootstrapper, accessing
    // ServiceLocator.Current here will throw an exception!
    // If you want access to IServiceLocator, resolve it from the container directly.
    base.ConfigureContainer();

    // Set up our own content loader, passing it a reference to the service locator
    // (it will need this to resolve ViewModels from the container automatically)
    this.Container.RegisterInstance<IRegionNavigationContentLoader>(
       new ViewModelContentLoader(this.Container.Resolve<IServiceLocator>()));
}

The ViewModelContentLoader itself derives from RegionNavigationContentLoader to reuse code, and will look something like this:

public class ViewModelContentLoader : RegionNavigationContentLoader
{
    private readonly IServiceLocator serviceLocator;

    public ViewModelContentLoader(IServiceLocator serviceLocator)
        : base(serviceLocator)
    {
        this.serviceLocator = serviceLocator;
    }

    // THIS IS CALLED WHEN A NEW VIEW NEEDS TO BE CREATED
    // TO SATISFY A NAVIGATION REQUEST
    protected override object CreateNewRegionItem(string candidateTargetContract)
    {
        // candidateTargetContract is e.g. "NameOfMyViewModel"

        // Just a suggestion, plug in your own resolution code as you see fit
        var viewModelType = this.GetTypeFromName(candidateTargetContract);
        var viewModel = this.serviceLocator.GetInstance(viewModelType);

        // get ref to viewResolver somehow -- perhaps from the container?
        var view = _viewResolver.FromViewModel(vm);

        return view;
    }

    // THIS IS CALLED TO DETERMINE IF THERE IS ANY EXISTING VIEW
    // THAT CAN SATISFY A NAVIGATION REQUEST
    protected override IEnumerable<object> 
    GetCandidatesFromRegion(IRegion region, string candidateNavigationContract)
    {
        if (region == null) {
            throw new ArgumentNullException("region");
        }

        // Just a suggestion, plug in your own resolution code as you see fit
        var viewModelType = this.GetTypeFromName(candidateNavigationContract);

        return region.Views.Where(v =>
            ViewHasDataContract((FrameworkElement)v, viewModelType) ||
            string.Equals(v.GetType().Name, candidateNavigationContract, StringComparison.Ordinal) ||
            string.Equals(v.GetType().FullName, candidateNavigationContract, StringComparison.Ordinal));
    }

    // USED IN MY IMPLEMENTATION OF GetCandidatesFromRegion
    private static bool 
    ViewHasDataContract(FrameworkElement view, Type viewModelType)
    {
        var dataContextType = view.DataContext.GetType();

        return viewModelType.IsInterface
           ? dataContextType.Implements(viewModelType)
           : dataContextType == viewModelType 
                   || dataContextType.GetAncestors().Any(t => t == viewModelType);
    }

    // USED TO MAP STRINGS OF VIEWMODEL TYPE NAMES TO ACTUAL TYPES
    private Type GetTypeFromName(string typeName)
    {
        // here you need to map the string type to a Type object, e.g.
        // "NameOfMyViewModel" => typeof(NameOfMyViewModel)

        return typeof(NameOfMyViewModel); // hardcoded for simplicity
    }
}
Laveta answered 18/9, 2011 at 16:14 Comment(1)
That's actually perfect! Thanks! - PjSurrey
A
2

To stop some confusion about "ViewModel first approach": You use more a "controller approach", but no "ViewModel first approach". A "ViewModel first approach" is, when you inject your View in your ViewModel, but you wire up both, your ViewModel and View, through a third party component (a controller), what by the way is the (I dont want to say "best", but) most loosely coupled approach.

But to answer your Question: A possible solution is to write an Extension for the Prism RegionManager that does exactly what you have described above:

    public static class RegionManagerExtensions
    {            
        public static void AddToRegion<TViewModel>(
               this IRegionManager regionManager, string region)
        {
            var viewModel = ServiceLocator.Current.GetInstance<TViewModel>();
            FrameworkElement view;

            // Get View depending on your conventions

            if (view == null) throw new NullReferenceException("View not found.");

            view.DataContext = viewModel;
            regionManager.AddToRegion(region, view);
            regionManager.Regions[region].Activate(view);

        }
    }

then you can call this method like this:

regionManager.AddToRegion<IMyViewModel>("MyRegion");
Adverse answered 17/9, 2011 at 12:28 Comment(2)
Isn't that pretty much exactly what I'm doing in my original example though? - What I'm after is a way to override / influence how the navigation api (ie, RequestNavigate()) resolves view instances from a UriSurrey
I've edited the question slightly to hopefully make it clearer!Surrey

© 2022 - 2024 — McMap. All rights reserved.