Derik Whittaker

Syndication

News


Convention Based View Model Location using Ninject in a MVVM/Silverlight Application
***** UPDATE *****
If you would like to see a video on this topic check out Episode 194 @ Dimecasts.Net
*******************

When building out a Silverlight application (or WPF application for that matter) the MVVM pattern is the pattern of choice for most projects I am aware of.  When using MVVM there is a sub pattern which is commonly used which is known as ViewModelLocator.  The ViewModelLocator concept basically tries to handle the repetitive task of coupling the View with its ViewModel but doing so in a way which removes any manual coding and it just ‘happens’.

Now if you have done any research on the ViewModel Location I am sure you have run across countless other blog posts out there which explain how to implement in this code, and many of them are great.  In fact here are a few which helped me down the path of creating the code I am going to talk about in this post

All of the above posts basically show how to implement this pattern in your application and from what I can tell all of them work and will do the job. However, none of those (minus the Caliburn.Micro) implementations fulfills the one requirement I want which is to totally remove the need to ‘wire’ the ViewModel up to your View if even via XAML.  I want to remove this wiring from the XAML because in my mind this still leaves me hard wiring the relationship rather than using convention based wiring.  One scenario in which the hard wiring could turn around and bite you is in the times where you rename the view and the view model.  If you fail to update your wiring code in XAML you will receive runtime errors.

Here are a few examples:
John Papa’s Implementation

DataContext="{Binding Source={StaticResource VMLocator}, Converter={StaticResource VMIndexerConverter}, ConverterParameter=MainPageViewModel}"

ViewModelLocator’s Implementation

    <UserControl.Resources>
        <ViewModels:EditUsersViewModelLocator x:Key="viewModelLocator"  />
    </UserControl.Resources>
    <UserControl.DataContext>
        <Binding Source="{StaticResource viewModelLocator}" Path="ViewModel" />
    </UserControl.DataContext>

I wanted my implementation of toe ViewModelLocator pattern to NOT need to know anything about the actual View or the ViewModel either in the XAML or in the code behind.  What I wanted as the following:

DataContext="{Binding Source={StaticResource VMLocator}, Converter={StaticResource VMIndexerConverter}}"

Now you may be looking at my XAML above and comparing it to the XAML from John Papa’s post and be thinking what is really different?  The difference is in this segment of code ConverterParameter=MainPageViewModel In his example he ‘hard wires’ the ViewModel which is being used up to the page and I did not want to do that.  I wanted to use a standard naming convention (right, convention over configuration) and allow my locator to determine the ViewModel based on the current View.

So how did I implement my ViewModelLocator pattern, well lets take a look.

Code for ViewModelLocator

public class ViewModelLocator
{   
    private static readonly IDictionary<type, type> s_viewModelCache = new Dictionary<type, type>();
        
    public Type GetViewModelTypeForView( Type viewType )
    {
        var viewModelName = ResolveViewModelName(viewType.Name).ToLower();

        // We are try to cache the types in order to remove the assembly scan hit, this is optional of course and 
	// you may not want or need this
	// One thing to point out here is that this code ASSUMES that your view and viewmodel classes are in the 
	//  same assembly.  if this is not true for your project this code will not work as expected.  We made this
	//  assumption because it is true in our project
        var foundViewModelType = GetExistingTypeFromCache(viewType) ?? (from type in viewType.Assembly.GetExportedTypes()
                                                                        where viewModelName.Equals(type.Name.ToLower())
                                                                        select type).FirstOrDefault();
    
        // this will throw a custom exception type, you can use your own if you want
        if (foundViewModelType == null) { throw new ViewModelTypeNotResolvedException(viewModelName, viewType); }

        // Add the found view and viewModel to the cache
        AddToCache(viewType, foundViewModelType);

        return foundViewModelType;
    }

    private void AddToCache(Type viewType, Type foundViewModelType)
    {
        if (!s_viewModelCache.ContainsKey(viewType))
        {
            s_viewModelCache.Add(viewType, foundViewModelType);
        }
    }

    private Type GetExistingTypeFromCache( Type viewType )
    {
        if (s_viewModelCache.ContainsKey(viewType))
        {
            return s_viewModelCache[viewType];
        }

        return null;
    }

    private string ResolveViewModelName(string viewTypeName)
    {
        var asLowerViewTypeName = viewTypeName.ToLower();

	// ***** Below is OUR product's current naming conventions, replace with your own if you want *****
        // check to see if the view ends w/ "View"
        if (asLowerViewTypeName.EndsWith("view"))
        {
            return asLowerViewTypeName.Replace("view", "ViewModel");
        }

        if (asLowerViewTypeName.EndsWith("usercontrol"))
        {
            return asLowerViewTypeName.Replace("usercontrol", "ViewModel");
        }

        if (asLowerViewTypeName.EndsWith("childwindow"))
        {
            return asLowerViewTypeName.Replace("childwindow", "ViewModel");
        }

        return string.Concat(viewTypeName, "ViewModel");
    }

    public static IDictionary<type, type> ViewModelCache { get { return s_viewModelCache; } }
}

Code for IndexConverter

public class IndexerConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 
    {
        Contract.Requires<ArgumentNullException>(value != null);

        var locator = value as ViewModelLocator;

        Contract.Assume(locator != null,
                        string.Format(
                            "The provided converter value was expected to be of type {0} but was provided as type {1} which is not acceptable for this converter",
                            typeof(ViewModelLocator).Name,
                            value.GetType().Name));

        var viewType = GetViewType();

        var viewModelType = locator.GetViewModelTypeForView(viewType);

	// We are using the ServiceLocator pattern which is wrapping Ninject
        var viewModelInstance = ServiceLocator.Current.GetInstance(viewModelType);

        return viewModelInstance;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
        
    // This does incur a perf hit by looking at the stack trace, but in 
    //  my opinion this hit is worth it compared to having to manually wire items up
    private Type GetViewType()
    {
        var stack = new StackTrace();

        return stack.GetFrames()
            .Select(f => f.GetMethod())
            .Where(m => m.Name == "InitializeComponent")
            .Select(m => m.DeclaringType)
            .FirstOrDefault();
    }

}

Code needed in your App.Xaml

<MVVMLib:ViewModelLocator x:Key="VMLocator" />
<MVVMLib:IndexerConverter x:Key="VMIndexerConverter" />

Code for the custom Exception ViewModelTypeNotResolvedException

public class ViewModelTypeNotResolvedException : Exception
{
    public string ViewModelName { get; set; }
    public Type ViewName { get; set; }

    // Commented this out because i do not want to be able to create this without the VMName or the VName
    //public ViewModelTypeNotResolvedException() {}

    /// 
    /// Will build an instance of the exception and push in a default message based on the viewModelname and view properties.
    /// *** This is the lazy way to create the message ***
    /// 
    /// The name of the viewModel which was not able to be created or found
    /// The type for the view we were trying to bind the VM to
    public ViewModelTypeNotResolvedException(string viewModelName, Type view)
        : base(string.Format("When trying to resolve the view model {0} it was not found in the same assembly as the view {1}", viewModelName, view.Name))
    {
        ViewModelName = viewModelName;
        ViewName = view;            
    }

    /// 
    /// Will build an instance of the exception
    /// 
    /// The name of the viewModel which was not able to be created or found
    /// The type for the view we were trying to bind the VM to
    /// 
    public ViewModelTypeNotResolvedException(string viewModelName, Type view, string message) : base(message)
    {
        ViewModelName = viewModelName;
        ViewName = view;
    }

    /// 
    /// Will build an instance of the exception
    /// 
    /// The name of the viewModel which was not able to be created or found
    /// The type for the view we were trying to bind the VM to
    /// 
    /// 
    public ViewModelTypeNotResolvedException(string viewModelName, Type view, string message, Exception innerException)
        : base(message, innerException)
    {
        ViewModelName = viewModelName;
        ViewName = view;
    }
}

As you can see the code needed for this is not that complicated or difficult to implement. 

One thing to understand with this post, I am NOT saying that the other implementations are bad or they do not work. In fact I am saying the exact opposite, they all are great and they all work and I used all their implementations as baseline for my implementation.  What I am saying as the other examples did not meet my EXACT needs.

Please take a look at the code and let me know where I went wrong or what your thoughts are. 

Till next time,


Posted 07-02-2011 6:23 AM by Derik Whittaker

[Advertisement]

Comments

Rob Eisenberg wrote re: Convention Based View Model Location using Ninject in a MVVM/Silverlight Application
on 07-02-2011 10:54 AM

I'd love to know about why the Caliburn.Micro locator was not sufficient...would you mind sending me an email with some feedback? Thanks!

Derik Whittaker wrote Hey, why is my ViewModelLocator not being called when creating a view?
on 08-04-2011 12:44 PM

Ok, this is going to seem really stupid, cause it was, but I thought I would share my pain incase someone

About The CodeBetter.Com Blog Network
CodeBetter.Com FAQ

Our Mission

Advertisers should contact Brendan

Subscribe
Google Reader or Homepage

del.icio.us CodeBetter.com Latest Items
Add to My Yahoo!
Subscribe with Bloglines
Subscribe in NewsGator Online
Subscribe with myFeedster
Add to My AOL
Furl CodeBetter.com Latest Items
Subscribe in Rojo

Member Projects
DimeCasts.Net - Derik Whittaker

Friends of Devlicio.us
Red-Gate Tools For SQL and .NET

NDepend

SlickEdit
 
SmartInspect .NET Logging
NGEDIT: ViEmu and Codekana
LiteAccounting.Com
DevExpress
Fixx
NHibernate Profiler
Unfuddle
Balsamiq Mockups
Scrumy
JetBrains - ReSharper
Umbraco
NServiceBus
RavenDb
Web Sequence Diagrams
Ducksboard<-- NEW Friend!

 



Site Copyright © 2007 CodeBetter.Com
Content Copyright Individual Bloggers

 

Community Server (Commercial Edition)