canton7 / Stylet

A very lightweight but powerful ViewModel-First MVVM framework for WPF for .NET Framework and .NET Core, inspired by Caliburn.Micro.
MIT License
995 stars 144 forks source link

Design-Time Binding support for Views #2

Closed keichinger closed 9 years ago

keichinger commented 9 years ago

In other MVVM frameworks such as MVVM Light, there is a ViewModelLocator class which is used to retrieve the ViewModels (View First) which allows you to specify the type of the DataContext to get design time support for all of your bindings. So far I haven't been able to find such feature in the docs. Is anything like that planned?

canton7 commented 9 years ago

I toyed with the idea of having a View -> ViewModel resolver specifically for design-time. In the end, it turned out to be too much overhead and complexity to be worth it.

You can use the d:DataContext attached property to specify the ViewModel to use at design-time (although how exactly to do this is very poorly documented on MSDN imo). This has other advantages, as well:

I've tried in the past to make d:DataContext a bit easier to use (it requires something like this at the moment - from memory, so might not be right!):

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:NamespaceContainingViewModels"
d:DataContext="{d:DesignInstance local:ViewModelType}"

... but I haven't been able to: I think the designer is looking specifically for d:DataContext, and won't accept an attached property which does the same thing.

I think the best option is probably to leave things as they are, but add some documentation on what exactly is and isn't supported at design-time, including how to use d:DataContext (and probably have another look at whether this can be simplified).

What do you think?

canton7 commented 9 years ago

Looking further into MVVM light's ViewModelLocator, it doesn't do quite what I thought. Let me look into it further...

canton7 commented 9 years ago

OK, so it looks like there's a possibility of providing a means of bootstrapping VMs for design-time use. So a VM can be agnostic of whether it's being used at design-time or runtime, the IoC container can take care of setting it up for runtime, and some other bootstrapper can set it up for design-time. This doesn't get away from the need to state the VM to use in the View's XAML (as a design-time attribute), but from what I've seen of ViewModelLocator use within MVVM light, that's nothing new.

The thing I really want to stay away from is automagically locating the ViewModel from the View. We've got enough magic going from ViewModel to View, without adding more.

I'm going to be pretty busy over the next few days, but I'll mull this over and see if anything sensible comes out. Any suggestions you've got are of course very welcome!

canton7 commented 9 years ago

Current thinking:

I'm toying with the idea of having the ApplicationLoader add the Boostrapper to the Application as a resource. This would let a custom ViewModelLocator have access to the Bootstrapper's IoC container, should it want it. I can't remember whether the Application.Startup event gets fired at design-time - if not, then the bootstrapper won't initialise its container anyway, and s:View.Model won't work... I'll run some tests, see what works out.

Thoughts?

canton7 commented 9 years ago

OK, turns out Application.Startup isn't fired at design-time - makes sense...

So we'll need to configure enough for the IViewManager to get resolved, so that s:View.Model doesn't crash out at design-time. We'll need to completely configure the IoC container in case the user's decided to override the registration for the IViewManager.

I'm thinking of having ConfigureIoC called at run- and design-time, but having separate Configure and ConfigureDesignTime methods. Although, Configure is the point at which extra assemblies are added, so we may need to call it... I just don't want to call anything that the user may be using to initialize services - that would be a dangerous default. Maybe having an 'isDesignTime' parameter to Configure would make it obvious that there's danger there. Or maybe document that OnStartup is a much better place to do that sort of stuff. Hmm...

keichinger commented 9 years ago

I like your thoughts so far. Another possible way, though probably not so nice as having an ConfigureDesignTime method or a parameter for the ConfigureIoC method, is to have a static property somewhere that determines whether or not the current code is executed from either the designer or the run-time. See IsInDesignMode and IsInDesignModeStatic https://mvvmlight.codeplex.com/SourceControl/latest#GalaSoft.MvvmLight/GalaSoft.MvvmLight (PCL)/ViewModelBase.cs

That way people could easily add basic logic to their ConfigureIo to add optional modules and/or classes that are only necessary at design-time.

There are probably way too many ways to accomplish a feature like this :)

According to some very old code of mine (which is using MVVM Light) all you need is the following to make the VMLocator working:

App.xaml

  <Application.Resources>
      <vm:ViewModelLocator xmlns:vm="using:Namespace..." x:Key="VMLocator" />
  </Application.Resources>

Inside your View:

  DataContext="{Binding Source={StaticResource VMLocator}, Path=PropertyNameOfViewModelInsideTheLocator}"

I think the trick here is that the ViewModelLocator has a static constructor which is picked up by the VS designer. That can then either initialize the IoC itself or simply set up the Locator to return hardcoded design-time ViewModels. I think for the sake of designing one doesn't necessarily need a full fledged IoC when working with VS or Blend - in the end it all comes down to your Models and ViewModels.

canton7 commented 9 years ago

There's Execute.InDesignMode (currently undocumented in the wiki), but I don't want to tell people "In this particular override, you must put appropriate guards around specific bits of your code, based on the output of a static method from an entirely unrelated class, or Bad Things you're Not Expecting will happen". Right now the temptation is to call all of Configure and ConfigureIoC, but not OnStartup, and make the docs very clear on what should go where. People are of course free to get more advanced here if they want, but the defaults need to be unsurprising...

That sort of ViewModelLocator is one I did see a couple of times when perusing MVVM Light blog posts. Is there much framework support needed to get something like that working? My impression was that we could just document how to write such a locator, and leave it up to the user to write it (and provide a sample in the Samples folder).

keichinger commented 9 years ago

In my humble opinion it's not really necessary to have it in the core of Stylet. If there's enough documentation then it's fine - at least for me. Maybe there could be a dedicated package in the future which can be implemented via NuGet's developer dependencies which has an extendable ViewModelLocator etc.

canton7 commented 9 years ago

I think the bit that I'm fuzzy on is what exactly would be in the extendible ViewModelLocator. What would this provide?

keichinger commented 9 years ago

I thought about a very basic class that takes care of the design time initialization which then gets extended by the user with dozens of ViewModel properties that Views can bind against. So purely for design time only; no other purposes.

canton7 commented 9 years ago

Yeah, I'm just trying to get clear in my head what that "design time initialization" comprises - which the bootloader doesn't (won't?) already do, and which is container-agnostic.

keichinger commented 9 years ago

I'm not sure if you really need a full bloated bootloader especially at design time where everything is a bit laggy (aka takes more time to render/load - at least for me) and more likely to crash due to unexpected behavior.

Or do you want to have some logic hidden somewhere that only initializes a specific part of the framework that might be needed at design time, such as fake viewmodel data?

canton7 commented 9 years ago

We need enough of the bootloader that the IViewManager implementation is loaded, otherwise uses of s:View.Model won't work at design-time. Since the user might swap in a different implementation in ConfigureIoC, we need to at least call that, and in order for that to work properly we need to have called Configure first - otherwise any extra assemblies to scan won't have been set.

I appreciate your point about the risks of unexpected/crashy behaviour rising as we run more and more of the bootloader though... Separating all design-time stuff into a separate place is nice, but forcing them to register their custom IViewManager twice (once in the bootloader, once in the custom design-time config place thingy) is not. Hmm....

keichinger commented 9 years ago

I don't see why the IViewManager would be necessary for design-time data: None of its magic is really required as the ViewModel(s) for a View have a one-to-one association which hopefully never changes, thus they can be easily hardcoded inside a single class, the ViewModelLocator.

Or is there anything important I'm missing? In the end you just wanna tell Visual Studio (or rather the designer) which ViewModel(s) is/are used for a certain View plus display some hardcoded fake data.

canton7 commented 9 years ago

As a simple example:

class MySubViewModel
{
}

class MyViewModel
{
   public MySubViewModel SubViewModel { get; set; }
}
<UserControl x:Class="SomeNamespace.MySubView" ...>
</UserControl>

<Window x:Class="SomeNamespace.MyView" ....>
<ContentContent s:View.Model="{Binding SubViewModel}"/>
</Window>

In order for the MySubView to be resolved and displayed inside the ContentControl, we need the IViewManager.

... If we go another route and just display a TextBlock saying "Embedded views are not supported at design time" or some such, then no, we don't need any of the bootstrapper infrastructure.

canton7 commented 9 years ago

Righty, there's a full example at https://github.com/canton7/Stylet/compare/develop...feature/design-time

What do you think @cH40z-Lord ? If that looks vaguely like what you were expecting, I'll write some docs.

keichinger commented 9 years ago

I've cloned the code locally and had a brief look at the Sample project. So far everything looks nice and complete :) Couldn't think of anything that didn't work or was hard to implement.

To wrap this up I'd say that you did a very good job implementing everything. Very well done - thank you! This is definitely a time saver.

canton7 commented 9 years ago

Sweet! Thanks for the prodding.

I need to get a bit more stuff into the base library in order to get the test coverage for the design-time branch up, then there's docs to write... Hopefully I'll get this merged in a week or so.