Caliburn.Micro icon indicating copy to clipboard operation
Caliburn.Micro copied to clipboard

Mapping "design-time" view models to matching views

Open superware opened this issue 9 years ago • 16 comments
trafficstars

I have a "DesignTime" prefix for every view model, and a special DesignTimeViewModelLocator that configures the IoC container accordingly. What is the most simple way to also support DesignTimeMyViewModel -> MyView convention without breaking default naming conventions? thanks.

superware avatar Nov 09 '16 10:11 superware

The easiest way at the moment would be to extend the methods on ViewModelLocator if we're in design time then do your own custom transform, otherwise return the regular result.

This way you're not affecting run time at all and can add your own custom convention.

nigel-sampson avatar Nov 09 '16 10:11 nigel-sampson

Problem is it's not that simple (I think) since I need ViewModels\DesignTimeMyViewModel -> Views\MyView, so it's basically the default transformation, only with a prefix that should be removed :)

Can you think of something more simple than duplicating default regular expression? thanks!

superware avatar Nov 09 '16 12:11 superware

Had a bit more of a think about this, and just wanted to double check your use case, currently in the view you actively have open the data context is set in the designer by the d:DataContext attribute. That view model creates child view models whose view (now fixed) is found an inserted in the designer.

Where do you see your design time view models being located by the designer and not set via d:DataContext?

nigel-sampson avatar Nov 09 '16 19:11 nigel-sampson

I'm using MEF and "View Model first", the view goes something like:

d:DataContext="{Binding Source={StaticResource DesignTimeViewModelLocator}, Path=[My.Modules.Shell.IShellViewModel]}"

And in the content:

<ContentControl cal:View.Model="{Binding SubViewModel}">

Where SubViewModel is a property of IShellViewModel that returns an ISubViewModel. The DesignTimeViewModelLocator resolves ISubViewModel to a ...ViewModels.DesignTimeSubViewModel but Caliburn Micro can't locate the matching view which is ...Views.SubView ("Cannot find view for ...ViewModels.DesignTimeSubViewModel").

Thanks.

superware avatar Nov 09 '16 19:11 superware

I think it would be cool to enhance the ViewModelLocator (or whatever else is needed) to allow additional DesignTime models to be searched for (when in DesignTime Mode). Why? Because I find it reasonable not using the "real" ViewModels sometimes (e.g. because I don't want to bloat them with DesignTime requirements (like parameterless constructor, default data sets for dummy representations etc.). At the same time I don't want to add some Runtime specific dependencies to the DesignTime. So it makes sense to separate those Models, especially if they are complex and share only a common Interface. A convenient default mapping that takes possible DesignTimeViewModels into account during DesignTime would be nice.

A sample suggestion: SomeNamespace.SomeViewModel => SomeNamespace.DesignTimeSomeViewModel => SomeNamespace.SomeView SomeNamespace.SomeViewModel => SomeNamespace.DesignTime.SomeViewModel => SomeNamespace.SomeView SomeNamespace.SomeViewModel => SomeNamespace.DesignTime.DesignTimeSomeViewModel => SomeNamespace.SomeView SomeNamespace.SomeViewModel => SomeNamespace.SomeDesignTimeViewModel => SomeNamespace.SomeView SomeNamespace.SomeViewModel => SomeNamespace.DesignTime.SomeDesignTimeViewModel => SomeNamespace.SomeView

I'm not sure about the ViewLocator and whether this class should have separate methods to register additional rules here ViewLocator.DesignNameTransformer.AddRule or the default will be enough... or a separate DesignViewLocator.NameTransformer?

Currently, I declare the concrete DesignTimeViewModel within the Xaml of the page using d:DataContext and d:DesignInstance: d:DataContext="{d:DesignInstance theApp:SomeViewModel, IsDesignTimeCreatable=True}"

Would be nice if this could be resolved dynamically (from the BootLoader). It is also nice that those can be resolved from different Assembly than runtime ViewModels.

beachwalker avatar Nov 09 '16 21:11 beachwalker

I don't believe we're going to be able to avoid the declaration of the view model at design time in xaml (at least for the root view), but it's something to look into.

For the child views / view models, these would end up being driven by the design time view model you've specified above.

What makes this interesting is that this means you don't need to do detection of design mode in your view models. You can wholly split it into a real view model creating real child view model and a design time view model (specified in the xaml) creating design time child view models.

At this point it's less about design time view model location, but view location for the child (design) view model.

I'll need to check a bit more about which platforms execute which code at design time, but it could be simply enough to use a Design sub-namepsace and the following setup code.

ViewLocator.AddNamespaceMapping("Company.App.ViewModels.Design", "Company.App.Views");

nigel-sampson avatar Nov 09 '16 22:11 nigel-sampson

I think you're right in the point that is less important to get rid of the Xaml-declaration of the DesignTime ViewModel and more about child models.

The namespace mapping is only a solution for the short term, I think. It would be more convenient if there is a builtin default convention, especially when you have several Namespaces to add. Or can you do something like ViewLocator.AddNamespaceMapping("{autoreplace}ViewModels.Design", "{autoreplace}.Views"))?

To have a default mapping is especially interesting because we use a shared Bootstrapper<T> that contains the default behaviour for all our applications (including plugin and viewmodel/view search directories, loading order, IoC etc.). Naturally, that Bootstrapper<T> has a derived instance like MyAppBootstrapper : Bootstrapper<IShell>in the App where where application specific overrides can be done, but these cases are very rare).

As an example:

internal sealed class Bootstrapper : Bootstrapper<IShell>
{
        // integration with own bootstrapping, not provided by Caliburn.Micro
    protected override IEnumerable<FileInfo> CollectViewAssemblyFilenames(DirectoryInfo applicationDirectory)
    {

        var searchPatterns = new List<string>
                                 {
                                     "MyApp.UI.*.dll",
                                     "MyApp.Components.Presentation.*.dll",
                                     "SomeOtherPlugins.Pages.*.dll"
                                 };

        return searchPatterns.SelectMany(applicationDirectory.GetFiles);
    }


    protected override IEnumerable<Assembly> SelectAssemblies()
    {
        var assemblies = new HashSet<Assembly>();

                // want to get rid of that, too...
        if (Execute.InDesignMode)
        {
            // todo: Register basic services required by Caliburn.Micro for DesignTime support
            var assembly = typeof(ParameterDesignViewModel).Assembly;
            if (!AssemblySource.Instance.Contains(assembly)) assemblies.Add(assembly);
            return assemblies;
        }

        return base.SelectAssemblies();
    }
}

But a typical should just looks like this

internal sealed class Bootstrapper : Bootstrapper<IShell>
{
}

and should not be bound to a specific ViewModel or anything else other than knowning the type of the main ViewModel (so to say MainWindw, which gets resolved by IoC). Beyond that most of them need no further customization. Adding ViewLocator.AddNamespaceMapping calls to this makes the Bootstrapper contain explicit knowledge about view specific handlings and being aware of the namespace where they reside. If you need this in all your Bootstrappers for the same thing (resolving DesignTime ViewModels and Children) it seems pretty clear to add such a default to the Bootstrapper<T> or Caliburn itself. But putting such a NamespaceMapping into Bootstrapper<T> does not fit the requirements of being independent of the Apps namespace where it is used.

Not sure this description is clear enough. But feel free to ask. :-)

beachwalker avatar Nov 10 '16 09:11 beachwalker

The AddNamepsaceMapping cannot except tokens in the manner you suggested, it's certainly not a solution for adding to the framework, but something I think that helps illustrate the scope of the feature.

Which in my mind is the following:

  1. At design time consider extra types for the location of view models. These would be the existing considered

For instance the view Sandbox.Views.ShellView has the following considered.

  • Sandbox.ViewsModels.ShellViewModel
  • Sandbox.ViewsModels.Shell

At design time the following would also be considered:

  • Sandbox.ViewsModels.Design.ShellViewModel
  • Sandbox.ViewsModels.Design.Shell

nigel-sampson avatar Nov 10 '16 09:11 nigel-sampson

@nigel-sampson I think you you meant Sandbox.ViewModels.Design.ShellViewModel, although I would go for something a bit more explicit like DesignTime or Designer, IMHO Design is a bit general.

How about introducing an IDesignTimeViewModelLocator or DesignTimeViewModelLocatorBase to the library? Together with any IoC container it will enable quick design time cascading view models support.

My current DesignTimeViewModelLocator:

public class DesignTimeViewModelLocator
{
    private readonly CompositionContainer _container;

    public DesignTimeViewModelLocator()
    {
        //mapping here all view model interfaces to design time classes such as My.Modules.Shell.ViewModels.DesignTimeShellViewModel etc.

        _container = new CompositionContainer(filteredCatalog);
    }

    public object this[string viewModelName]
    {
        get
        {
            return _container.GetExportedValue<object>(viewModelName);
        }
    }
}

App.xaml:

<dt:DesignTimeViewModelLocator x:Key="DesignTimeViewModelLocator" />

And each view:

d:DataContext="{Binding Source={StaticResource DesignTimeViewModelLocator}, Path=[My.Modules.Shell.IShellViewModel]}"

And

<ContentControl cal:View.Model="{Binding SubViewModel}">

superware avatar Nov 10 '16 11:11 superware

Thanks for the catch, have updated.

I'm not sure about the rest, will have a think about it.

Chose the Design keyword since the follows the design time assemblies convention,

nigel-sampson avatar Nov 11 '16 03:11 nigel-sampson

Besides the suggestion to introduce a design time locator, can you please change the default mapping to support ViewModels.Design.MyViewModel -> Views.MyView ?

Thank you for everything.

superware avatar Nov 11 '16 06:11 superware

@nigel-sampson How should I manually add a generic *.ViewModels.Design.MyViewModel -> *.Views.MyView? thanks.

superware avatar Nov 13 '16 19:11 superware

Quite interesting feature! Been struggling with this for quite some time, and actually solved with custom location strategy + custom Attribute on design-time viewModel to reference its run-time alter ego.

BrainCrumbz avatar Nov 16 '16 15:11 BrainCrumbz

That;'s an interesting idea, I'd probably go with convention but with a hook for changing if necessary.,

nigel-sampson avatar Nov 23 '16 23:11 nigel-sampson

There is a solution using a convention without changing code documented in issue #459.

beachwalker avatar Jul 04 '17 14:07 beachwalker

Thanks @beachwalker, looks good, I'll have to have a play.

nigel-sampson avatar Jul 19 '17 10:07 nigel-sampson