How to have design time support for nested view models in Caliburn Micro? #214

Closed
stajs opened this Issue Aug 19, 2015 · 13 comments

Projects

None yet

8 participants

@stajs
stajs commented Aug 19, 2015

I previously asked this question on SO, and a commenter suggested I ask here. Original SO:

http://stackoverflow.com/questions/29795367/how-to-have-design-time-support-for-nested-view-models-in-caliburn-micro


Using VS2013 and Caliburn.Micro 2.0.2

Given this example of:

  1. a shell view model that has a nested view model property, and
  2. both the shell and nested view models having a Name property:

Project

It seems that during design time the nested view model property is ignored. Is there a way to support this?

public class NestedViewModel : PropertyChangedBase
{
    private string _name;

    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            NotifyOfPropertyChange(() => Name);
        }
    }

    public NestedViewModel()
    {
        Name = "Nested";
    }
}

 

<UserControl
    x:Class="WpfApplication1.Views.NestedView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:cal="http://www.caliburnproject.org"
    xmlns:viewModels="clr-namespace:WpfApplication1.ViewModels"
    mc:Ignorable="d"
    d:DataContext="{d:DesignInstance Type=viewModels:NestedViewModel, IsDesignTimeCreatable=True}"
    cal:Bind.AtDesignTime="True">
    <Grid>
        <Label x:Name="Name" FontSize="16" Background="LightGreen"/>
    </Grid>
</UserControl>

The green label shows the correct Name for the nested view model in the designer:

Nested

public class ShellViewModel : PropertyChangedBase
{
    private string _name;
    private NestedViewModel _nestedViewModel;

    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            NotifyOfPropertyChange(() => Name);
        }
    }

    public NestedViewModel NestedViewModel
    {
        get { return _nestedViewModel; }
        set
        {
            if (Equals(value, _nestedViewModel)) return;
            _nestedViewModel = value;
            NotifyOfPropertyChange(() => NestedViewModel);
        }
    }

    public ShellViewModel()
    {
        NestedViewModel = new NestedViewModel();
        Name = "Shell";
    }
}

 

<UserControl
    x:Class="WpfApplication1.Views.ShellView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:viewModels="clr-namespace:WpfApplication1.ViewModels"
    xmlns:cal="http://www.caliburnproject.org"
    mc:Ignorable="d"
    d:DataContext="{d:DesignInstance Type=viewModels:ShellViewModel, IsDesignTimeCreatable=True}"
    cal:Bind.AtDesignTime="True">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <ContentControl Grid.Row="0" x:Name="NestedViewModel"/>
        <Label Grid.Row="1" x:Name="Name" FontSize="16" Background="RoyalBlue"/>
    </Grid>
</UserControl>

The green label should show the nested view model Name property in the designer, but instead shows the value of the shell view model:

Shell

It does bind correctly at runtime:

Run time

Example

@nigel-sampson
Contributor

Thanks for the bug report.

@nigel-sampson nigel-sampson added the bug label Aug 19, 2015
@stajs
stajs commented Aug 19, 2015

No problem.

I should add a note from the commentary too: when ShellViewModel and NestedViewModel have different property names, you will only see the blue label (ShellViewModel).

@tibel
Contributor
tibel commented Oct 2, 2015

In VS2015 the same sample shows following in the designer.

NestedView:
nestedview_vs2015

ShellView:
shellview_vs2015

Seems that the VS2015 designer behaves different.
My impression is that the bootstrapper code is not executed.

@quetzalcoatl
quetzalcoatl commented Jul 4, 2016 edited

I got both of above-mentioned behaviors in current 3.0.1 version.

I upgraded a project that was using 2.x on VS2012/3 version to 3.0.1 on VS2015, and at first I got the 'empty box' behavior - and indeed the bootstrapper code was not ran.

In my app I had the bootstrapper.Initialize() pulled out of bootstrapper's constructor and called it manually*) in app.xaml.cs, so in RunTime it worked fine and I'm pretty sure it also half-magically worked on DesignTime. But in VS2015 with 3.0.1 it started to work in RunTime only and in DesignTime it displayed only empty boxes instead of subviews.

When I moved 'Initialize()' back to bootstrapper's constructor and did a clean&rebuild, it presented "cannot find view for xxViewModel". It began to work after restarting visual studio, and now it I get the behavior from the first post - designtime bindings seem to pull data from outer context, not from the nested context.

*) small hack to prevent hard-to-read XamlParseExceptions (which also sometimes caused VS2012 to crash) and to try-catch any exceptions thrown during container setup and show a nice messagebox to the user (instead of insta-crash).

@nugarin
nugarin commented Jul 14, 2016

Hello.

I can confirm that this issue is present in the VS2015 with Caliburn.Micro 3.0.1

I am following a viewmodel-first pattern, with "Design" subclasses for viewmodels.

Example:

        public MainViewModel(IEventAggregator eventAggregator, ILogger log, ApplicationEventsViewModel applicationEventsViewModel) : base(eventAggregator)
        {
            ApplicationEventsViewModel = applicationEventsViewModel;
            _log = log;
            _log?.Info("Initialised");
        }

        public class Design:MainViewModel
        {
            private string _testBinding;
            private string _designTest;

            public Design():base(null, null, new ApplicationEventsViewModel(null))
            {
                DesignTest = "I am in main";

                ApplicationEventsViewModel.ApplicationEvents.Add(new ApplicationEvent()
                {
                    Message = "why no work??"
                });

            }

    ....

     public class Design : ApplicationEventsViewModel
        {
            private string _designTest;

            public string DesignTest
            {
                get { return _designTest; }
                set
                {
                    if (value == _designTest) return;
                    _designTest = value;
                    NotifyOfPropertyChange(() => DesignTest);
                }
            }

            public Design() : base(null)
            {
                DesignTest = "werk dammit!";

                ApplicationEvents.Insert(0, new ApplicationEvent
                {
                    EventTime = DateTime.Now.AddMinutes(-1),
                    Message = "<First Message>"
                });

Given viewmodels "MainViewModel" and it's nested viewmodel "ApplicationEventsViewModel".
Both work correctly in the design time view with their own properties.
However, bindings in the nested view try to bind to the main view model.

Textblock with name "DesignTest" will show "werk dammit", and the events from the ApplicationEvents collection when I open ApplicationEventsView
ApplicationEventsView that is hosted in a contentcontrol inside MainView, shows "I am in main" and no events.

@nugarin
nugarin commented Jul 14, 2016

I am also using a custom ViewModelLocator to allow thse subclasses, but I doubt this is causing the problem..


    protected override void StartDesignTime()
        {
            LogManager.GetLog = type => new NullLogger();

            var storedLocator = ViewLocator.LocateForModelType;

            ViewLocator.LocateForModelType = (type, o, arg3) =>
            {
            //    Debugger.Break();
                var res = type != null && type.Name.EndsWith("Design")
                    ? storedLocator(type.BaseType, o, arg3)
                    : storedLocator(type, o, arg3);

                return res;
            };

            base.StartDesignTime();
        }

cal:Bind.AtDesignTime="True"
        d:DataContext="{d:DesignInstance Type=viewModels:MainViewModel+Design, IsDesignTimeCreatable=True}"

...
d:DataContext="{d:DesignInstance Type=viewModels:ApplicationEventsViewModel+Design, IsDesignTimeCreatable=True}"
             cal:Bind.AtDesignTime="True"


@beachwalker

Is this somewhere in the schedule?

@nigel-sampson
Contributor

I've been having trouble recreating a lot of these issues, different behaviors on different versions of the designers don't help either.

Getting rather sick of their flaky behavior.

@eogas
eogas commented Oct 14, 2016

@nigel-sampson When you have time, can you try to reproduce this issue with the reproducer case I've created here: https://github.com/eogas/CaliburnMicroDesignTimeNestedBindingBug

I tried to boil it down to the simplest possible example, and I've been able to reproduce it consistently on multiple machines with VS2015 Update 3. I hadn't considered that there is different behavior in different VS versions. I should be able to test it on VS2012 later today.

@superware

@eogas say, have you tested this on VS2012? thanks.

@nigel-sampson
Contributor

I've pushed a fix that resolves this in VS2013, would appreciate some testing in VS2012 if anyone can help out.

@nigel-sampson nigel-sampson added this to the v3.0.2 milestone Nov 9, 2016
@eogas
eogas commented Nov 9, 2016 edited

@superware @nigel-sampson Sorry for the delay, I just tested out my reproducer against this change on VS2012 Professional with Update 4 and it appears to fix the issue as intended.

EDIT: It also works in VS2015 with Update 3 FWIW.

@nigel-sampson
Contributor

Thanks for the help. Glad to hear it's resolved in VS 2012.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment