DEV Community

Anu Viswan
Anu Viswan

Posted on

View Model First Approach using Caliburn Micro

While it is possible to use both View Model First and View First approach while using Caliburn Micro, I personally feel one should stick to one single appraoch thoughout your application. Mixing the two approaches would impact the readability of code adversely.

View Model First Approach
In this post we will look at the ViewModel first approach, which is the default approach used by Caliburn Micro. Simply stated, it uses ViewModel to recognize the associated View.

Let us assume we have a ShellViewModel class, which contains an instance of UserProfileViewModel, defined as in example code below.

public class ShellViewModel:Conductor<object>
{
    public UserProfileViewModel UserProfile { get; set; }

    public ShellViewModel()
    {
        UserProfile = IoC.Get<UserProfileViewModel>();
        UserProfile.Name = "Anu Viswan";
        UserProfile.Age = 37;
    }
}
Enter fullscreen mode Exit fullscreen mode

The Xaml part, particularly the View detection is quite simpler here, thanks to the Caliburn Micro's conventions.

<ContentControl  x:Name="UserProfile"/>
Enter fullscreen mode Exit fullscreen mode

The caliburn micro would detect the required View using the ViewLocator.LocateForModelType method. Following is how it looks like.

public static Func<Type, DependencyObject, object, UIElement> LocateForModelType = (modelType, displayLocation, context) =>{
    var viewTypeName = modelType.FullName.Replace("Model", string.Empty);
    if(context != null)
    {
        viewTypeName = viewTypeName.Remove(viewTypeName.Length - 4, 4);
        viewTypeName = viewTypeName + "." + context;
    }

    var viewType = (from assmebly in AssemblySource.Instance
                    from type in assmebly.GetExportedTypes()
                    where type.FullName == viewTypeName
                    select type).FirstOrDefault();

    return viewType == null
        ? new TextBlock { Text = string.Format("{0} not found.", viewTypeName) }
        : GetOrCreateViewType(viewType);
};
Enter fullscreen mode Exit fullscreen mode

As you can observe, the method removes Model from the full name of the viewmodel to recognize the associate view. The parameter context brings us to an interesting scenario, which we will discuss a bit later. But for now, it becomes clear that how the naming conventions of Caliburn Micro works under the hood.

Custom naming conventions is easily possible with Caliburn Micro. But I guess that is another topic, which need's a post of its own.

Binding to Collection of ViewModel

For now, let us look into another scenario. Let us assume, we have a collection of ViewModels which needs to be displayed in an ItemsControl.

public class ShellViewModel:Conductor<object>
{
    public IEnumerable<UserProfileViewModel> UserProfileCollection { get; set; }
    public ShellViewModel()
    {
        UserProfileCollection = Enumerable.Range(1, 10).Select(x => new UserProfileViewModel { Name = $"Sample Name {x}", Age = 37 + x });
    }
}
Enter fullscreen mode Exit fullscreen mode

Binding the collection to ItemsControl and displaying each of the Items in a ContentControl would require a minor change. The usage of View.Model attached property, which is defined as

public static DependencyProperty ModelProperty =
            DependencyPropertyHelper.RegisterAttached(
                "Model",
                typeof(object),
                typeof(View),
                null,
                OnModelChanged
                );


public static void SetModel(DependencyObject d, object value) {
    d.SetValue(ModelProperty, value);
}

public static object GetModel(DependencyObject d) {
    return d.GetValue(ModelProperty);
}

static void OnModelChanged(DependencyObject targetLocation, DependencyPropertyChangedEventArgs args)
{
    if (args.OldValue == args.NewValue) {
        return;
    }

    if (args.NewValue != null) {
        var context = GetContext(targetLocation);

        var view = ViewLocator.LocateForModel(args.NewValue, targetLocation, context);
        ViewModelBinder.Bind(args.NewValue, view, context);
        if (!SetContentProperty(targetLocation, view)) {

            Log.Warn("SetContentProperty failed for ViewLocator.LocateForModel, falling back to LocateForModelType");

            view = ViewLocator.LocateForModelType(args.NewValue.GetType(), targetLocation, context);

            SetContentProperty(targetLocation, view);
        }
    }
    else {
        SetContentProperty(targetLocation, args.NewValue);
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can observe, the View.Model attached property, under the hood uses the ViewLocator.LocateForModel method itself, which we had previously seen.

We have now seen how View.Model is defined, so let us go ahead and write our xaml to finish off the example.

<ItemsControl x:Name="UserProfileCollection">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Border Grid.Row="1" BorderThickness="1" BorderBrush="LightGray">
                <ContentControl cal:View.Model="{Binding}"/>
            </Border>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>
Enter fullscreen mode Exit fullscreen mode

The Context Parameter - Multiple Views for single View Model

We had skipped the Context Parameter for the ViewLocator.LocateForModel method earlier. Let us now examine the role of the parameter in detail.

var viewTypeName = modelType.FullName.Replace("Model", string.Empty);
if(context != null)
{
    viewTypeName = viewTypeName.Remove(viewTypeName.Length - 4, 4);
    viewTypeName = viewTypeName + "." + context;
}
Enter fullscreen mode Exit fullscreen mode

From the above code (from the ViewLocator.LocateForModel), it is obvious that if the context parameter is non-null value, then it would replace the View string with a . followed by the context string. With that in mind, let us build our alternative views using the following folder structure.

Alt Text

Let us now pass the context parameter in our example above using the attached property.

<ItemsControl.ItemTemplate>
    <DataTemplate>
        <Border Grid.Row="1" BorderThickness="1" BorderBrush="LightGray">
            <ContentControl cal:View.Model="{Binding}" cal:View.Context="StudentProfile"/>
        </Border>
    </DataTemplate>
</ItemsControl.ItemTemplate>
Enter fullscreen mode Exit fullscreen mode

The UserProfileView would be now replaced with StudentProfile view which we have created in the previous step.

Conclusion
In this post, we examined how to name resolution happens behind the scene for Caliburn Micro in a View Model First approach. We will examine View First approach in a later post, but if you understand the difference between the approach, and the working of one, it becomes easier to expect the other should behave.

Top comments (2)

Collapse
 
decafprogrammer profile image
decaf-programmer

I followed your guide. but my app is saying like this..
Cannot find view for ViewModels.
How to solve this problem?

Collapse
 
anuviswan profile image
Anu Viswan • Edited

Could you please check if the folder structures are named correctly - by default the naming conventions require the ViewModels and Views in sub folders of its own, though you could override it.

Also please let me know if there is a way I could look at your code so that I could understand the problem better

PS: Apologies for the delay, hadn't seen this comment