Question

In my Viewmodel I have the properties LoggedInAs of type string and EditMode of type bool. I also have a List property called ReaderList which I bind to an ItemsControl for display purposes like this:

<ItemsControl Name="ReaderList" ItemTemplateSelector="{StaticResource drts}"/>

I am using Caliburn.Micro, so the Binding is done automatically by the naming. I want to use a DataTemplateSelector because if the application is in EditMode and the Person is the one that is logged in I want a fundamentally different display. So here is my declaration of the resources,

<UserControl.Resources>
    <DataTemplate x:Key="OtherPersonTemplate"> ... </DataTemplate>
    <DataTemplate x:Key="CurrentUserIsPersonTemplate"> ...  </DataTemplate>

    <local:DisplayReaderTemplateSelector x:Key="drts" 
           IsLoggedInAs="{Binding LoggedInAs}" 
           IsEditMode="{Binding EditMode}" 
           CurrentUserTemplate="{StaticResource CurrentUserIsPersonTemplate}"
           OtherUserTemplate="{StaticResource OtherPersonTemplate}"/>
</UserControl.Resources>

and here the code for the class:

public class DisplayReaderTemplateSelector: DataTemplateSelector {
    public DataTemplate CurrentUserTemplate { get; set; }
    public DataTemplate OtherUserTemplate { get; set; }

    public string IsLoggedInAs {get; set;}
    public bool IsEditMode { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container){
        var _r = item as Person;
        if (IsEditMode && _r.Name == IsLoggedInAs) return CurrentUserTemplate;
        else return OtherUserTemplate;
    }
}

For some reason the application crashes while instantiating the Viewmodel (resp. the View). Where is the error, and/or how could I solve this problem alternatively?

EDIT: The Crash was due to the binding expressions in the construction of the DisplayReaderTemplateSelector - because IsLoggedIn and EditMode are not DependencyProperties.

So the question now is: how can I have a DataTemplateSelector that depends on the status of the ViewModel if I cannot bind to values?

Was it helpful?

Solution

Whilst you could use a DataTemplateSelector or something of that ilk, it probably won't surprise you to find that in Caliburn.Micro has this functionality built-in in the form of View.Context and the ViewLocator

On your VM you can create a property which provides a context string which CM will use to resolve the View - since it uses naming conventions, you just need to provide the correct namespace/name for the sub-view along with a context string for it to locate an alternative view

In your VM you can create a context property that uses the user details to determine its value:

i.e.

public class SomeViewModel
{
    public string Context 
    {
        get 
        { 
            if (IsEditMode && _r.Name == IsLoggedInAs) return "Current";
            else return "Other";
        }
    }  

    // ... snip other code
}

The only problem I see (one that probably has a workaround) is that you want to determine the view from inside a ViewModel - usually you determine the context higher up and pass that to a ContentControl and CM uses it when locating the view for that VM

e.g.

your main VM:

public class MainViewModel
{
    public SomeSubViewModel { get; set; } // Obviously would be property changed notification and instantiation etc, I've just left it out for the example
}

and associated view

<UserControl>
    <!-- Show the default view for this view model -->
    <ContentControl x:Name="SomeSubViewModel" />
    <!-- Show an alternative view for this view model -->
    <ContentControl x:Name="SomeSubViewModel" cal:View.Context="Alternative" />
</UserControl>

then your VM naming structure would be:

- ViewModels
|
----- SomeSubViewModel.cs
    |
    - SomeSubView.xaml
    |
    - SomeSubView
    |
    ----- Alternative.xaml

and CM would know to look in the SomeSubView namespace for a control called Alternative based on the original VM name and the Context property (SomeSubViewModel minus Model plus dot plus Context which is SomeSubView.Alternative)

So I'd have to have a play around as this is the standard way of doing it. If you were to do it this way you'd have to either create a sub viewmodel and add a ContentControl to your view and bind the View.Context property to the Context property on the VM, or add the Context property higher up (to the parent VM).

I'll look at some alternatives - if there is no way to get the current ViewModel to decide its view based on a property using standard CM, you could customise the ViewLocator and maybe use an interface (IProvideContext or somesuch) which provides the ViewLocator with a context immediately -(I don't think you can't hook directly into the view resolution process from a VM)

I'll come back with another answer or an alternative shortly!

EDIT:

Ok this seems to be the most straightforward way to do it. I just created an interface which provides Context directly from a VM

public interface IProvideContext
{
    string Context { get; }
}

Then I customised the ViewLocator implementation (you can do this in Bootstrapper.Configure()) to use this if no context was already specified:

ViewLocator.LocateForModel = (model, displayLocation, context) =>
{
    var viewAware = model as IViewAware;

    // Added these 3 lines - the rest is from CM source
    // Try cast the model to IProvideContext
    var provideContext = model as IProvideContext;

    // Check if the cast succeeded, and if the context wasn't already set (by attached prop), if we're ok, set the context to the models context property
    if (provideContext != null && context == null)
         context = provideContext.Context;

    if (viewAware != null)
    {                    
        var view = viewAware.GetView(context) as UIElement;
        if (view != null)
        {
#if !SILVERLIGHT && !WinRT
            var windowCheck = view as Window;
            if (windowCheck == null || (!windowCheck.IsLoaded && !(new WindowInteropHelper(windowCheck).Handle == IntPtr.Zero)))
            {
                LogManager.GetLog(typeof(ViewLocator)).Info("Using cached view for {0}.", model);
                return view;
            }
#else
            LogManager.GetLog(typeof(ViewLocator)).Info("Using cached view for {0}.", model);
            return view;
#endif
        }
    }

    return ViewLocator.LocateForModelType(model.GetType(), displayLocation, context);
};

This should work for you and allows you to set the context directly on the target ViewModel - obviously this will probably only work for a View-First approach

So all you need to do is structure your views as I showed above (the correct namespaces etc) then set the Context property on your VM based on the value of IsLoggedInAs and EditMode

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top