ViewLocator NameTransformer help

Jul 14, 2011 at 9:57 AM

I'm using Caliburn Micro 1.1 and am making a WP7 application which has a Phone project (where the Views are) and a shared library which holds the View Models (among other things).

Here are the assembly names and namespaces of the views.

Project.Phone.dll

Project.Phone.Views.NameView

Project.Phone.Common.dll

Project.Phone.Common.ViewModels.NameViewModel

I have overriden SelectAssemblies correctly in the bootstrapper, but can't for the life of me get my head around the pattern matching required with the ViewLocator.NameTransformer.Add method. Anyone able to suggest how to do this easily so it all works automagically?

DebugLogger shows the following...

View Model not found. Searched: Project.Phone.ViewModels.MainPageViewModel, Project.Phone.ViewModels.MainPage, Project.Phone., Project.Phone..

Coordinator
Jul 14, 2011 at 1:43 PM

There's some documentation here: http://caliburnmicro.codeplex.com/wikipage?title=Using%20the%20NameTransformer&referringTitle=Documentation

If all else fails, you can still bypass the NameTransformer and replace the Funcs that do the name mapping. For phone, you'll need to make these changes in the ViewModelLocator and the ViewLocator.

Jul 15, 2011 at 1:10 AM

Aha. I'm getting an identical issue. Everything worked swimmingly in 1.0 with different assemblies. I'm interested to know what has changed to break it.

Jul 15, 2011 at 3:12 AM

I think there's a bigger problem. After changing some namespaces to match expectations, I'm finding that MainPage.xaml locates MainPageViewModel.cs without problem

BUT TopicDetail.xaml results in View Model not found. Searched: NZTechEdWP7.Client.Views.TopicDetailModel

What happened to "view"?!

I'm going to dig in and debug and will report back.

Coordinator
Jul 15, 2011 at 2:21 PM

BTW, we had some bugs in our resolution logic in v1.1. So, you should be using what's in the repo before you try to debug because the issue may have already been fixed.

Jul 18, 2011 at 10:02 PM
Edited Jul 18, 2011 at 10:12 PM
keithpatton wrote:

I'm using Caliburn Micro 1.1 and am making a WP7 application which has a Phone project (where the Views are) and a shared library which holds the View Models (among other things).

Here are the assembly names and namespaces of the views.

Project.Phone.dll

Project.Phone.Views.NameView

Project.Phone.Common.dll

Project.Phone.Common.ViewModels.NameViewModel

I have overriden SelectAssemblies correctly in the bootstrapper, but can't for the life of me get my head around the pattern matching required with the ViewLocator.NameTransformer.Add method. Anyone able to suggest how to do this easily so it all works automagically?

DebugLogger shows the following...

View Model not found. Searched: Project.Phone.ViewModels.MainPageViewModel, Project.Phone.ViewModels.MainPage, Project.Phone., Project.Phone..

Are you using v1.1 or the latest code from trunk? The current code from the trunk will yield the following:

Given: Project.Phone.Views.MainPageView

Searched:  Project.Phone.ViewModels.MainPageViewModel,Project.Phone.ViewModels.MainPage,Project.Phone.ViewModels.IMainPageViewModel,Project.Phone.ViewModels.IMainPage

As you can see, the built-in transformation assumes the same namespace pattern for both the View and ViewModel:

Something.Something.Views.Something.SomeView => Something.Something.ViewModels.Something.SomeViewModel

Your ViewModel namespace has an extra "Common" that the View namespace doesn't have.

You can add the following code to your bootstrapper to handle your special case:

            //Check for <Namespace>.Views.<BaseName>View construct
            ViewModelLocator.NameTransformer.AddRule
                (
                    @"(?<nsbefore>([A-Za-z_]\w*\.)*)?(?<nsview>Views\.)(?<nsafter>[A-Za-z_]\w*\.)*(?<basename>[A-Za-z_]\w*)(?<suffix>View$)",
                    new[] {
                        @"${nsbefore}Common.ViewModels.${nsafter}${basename}ViewModel",
                        @"${nsbefore}Common.ViewModels.${nsafter}${basename}",
                        @"${nsbefore}Common.ViewModels.${nsafter}I${basename}ViewModel",
                        @"${nsbefore}Common.ViewModels.${nsafter}I${basename}"
                    },
                    @"(([A-Za-z_]\w*\.)*)?Views\.([A-Za-z_]\w*\.)*[A-Za-z_]\w*View$"
                );

            //Check for <Namespace>.Views.<BaseName><ViewSynonym> construct
            ViewModelLocator.NameTransformer.AddRule
                (
                    @"(?<nsbefore>([A-Za-z_]\w*\.)*)?(?<nsview>Views\.)(?<nsafter>[A-Za-z_]\w*\.)*(?<basename>[A-Za-z_]\w*)(?<suffix>Page$)",
                    new[] {
                        @"${nsbefore}Common.ViewModels.${nsafter}${basename}${suffix}ViewModel",
                        @"${nsbefore}Common.ViewModels.${nsafter}I${basename}${suffix}ViewModel"
                    },
                    @"(([A-Za-z_]\w*\.)*)?Views\.([A-Za-z_]\w*\.)*[A-Za-z_]\w*Page$"
                );


            //Check for <Namespace>.ViewModels.<BaseName>ViewModel construct
            ViewLocator.NameTransformer.AddRule
                (
                    @"(?<nsbefore>([A-Za-z_]\w*\.)*)?(?<nsvm>Common\.ViewModels\.)(?<nsafter>[A-Za-z_]\w*\.)*(?<basename>[A-Za-z_]\w*)(?<suffix>ViewModel$)",
                    @"${nsbefore}Views.${nsafter}${basename}View",
                    @"(([A-Za-z_]\w*\.)*)?Common\.ViewModels\.([A-Za-z_]\w*\.)*[A-Za-z_]\w*ViewModel$"
                );

            //Check for <Namespace>.ViewModels.<BaseName>PageViewModel construct
            ViewLocator.NameTransformer.AddRule
                (
                    @"(?<nsbefore>([A-Za-z_]\w*\.)*)?(?<nsvm>Common\.ViewModels\.)(?<nsafter>[A-Za-z_]\w*\.)*(?<basename>[A-Za-z_]\w*)(?<suffix>PageViewModel$)",
                    @"${nsbefore}Views.${nsafter}${basename}Page",
                    @"(([A-Za-z_]\w*\.)*)?Common\.ViewModels\.([A-Za-z_]\w*\.)*[A-Za-z_]\w*PageViewModel$"
                );

This will yield the following transforms:

Project.Phone.Views.MainPageView => Project.Phone.Common.ViewModels.MainPageViewModel, Project.Phone.Common.ViewModels.MainPage, Project.Phone.Common.ViewModels.IMainPageViewModel, Project.Phone.Common.ViewModels.IMainPage

Project.Phone.Common.ViewModels.MainPageViewModel => Project.Phone.Views.MainPage

Jul 18, 2011 at 10:30 PM
Edited Jul 19, 2011 at 6:08 AM
nzben wrote:

I think there's a bigger problem. After changing some namespaces to match expectations, I'm finding that MainPage.xaml locates MainPageViewModel.cs without problem

BUT TopicDetail.xaml results in View Model not found. Searched: NZTechEdWP7.Client.Views.TopicDetailModel

What happened to "view"?!

I'm going to dig in and debug and will report back.


It's not that you don't have the Views in the proper namespace, it's the naming of the View itself that is problematic. The more stringent built-in rules now require that Views either end with "View" or "Page". You can the following transform rules to your bootstrapper to get what you want:

            //Check for <Namespace>.<BaseName><ViewSynonym> construct
            ViewModelLocator.NameTransformer.AddRule
                (
                    @"(?<namespace>(.*\.)*)(?<basename>[A-Za-z_]\w*)(?<suffix>(Page$)|(Detail$))",
                    new[] {
                        @"${namespace}${basename}${suffix}ViewModel",
                        @"${namespace}I${basename}${suffix}ViewModel"
                    },
                    @"(.*\.)*[A-Za-z_]\w*((Page$)|(Detail$))"
                );


            //Check for <Namespace>.Views.<BaseName><ViewSynonym> construct
            ViewModelLocator.NameTransformer.AddRule
                (
                    @"(?<nsbefore>([A-Za-z_]\w*\.)*)?(?<nsview>Views\.)(?<nsafter>[A-Za-z_]\w*\.)*(?<basename>[A-Za-z_]\w*)(?<suffix>(Page$)|(Detail$))",
                    new[] {
                        @"${nsbefore}ViewModels.${nsafter}${basename}${suffix}ViewModel",
                        @"${nsbefore}ViewModels.${nsafter}I${basename}${suffix}ViewModel"
                    },
                    @"(([A-Za-z_]\w*\.)*)?Views\.([A-Za-z_]\w*\.)*[A-Za-z_]\w*((Page$)|(Detail$))"
                );

This will yield the following transform:

NZTechEdWP7.Client.Views.TopicDetail => NZTechEdWP7.Client.ViewModels.TopicDetailViewModel, NZTechEdWP7.Client.ViewModels.ITopicDetailViewModel

If you look at the code above, you can see that there is an "or" pattern for synonyms of "View" (i.e. "Page" and "Detail"). If you want to add more, you can change all instances of:

(Page$)|(Detail$)

with:

(Page$)|(Detail$)|(AnotherSynonym$)

Jul 18, 2011 at 10:52 PM

Thanks for this. Can I suggest an idea?

for simplicity, it would be nice to express the mapping in a very simple way like:

view root namespace = project.views

Vm root namespace = project.common.viewmodels

view class suffix = view (default)

vm class  suffix = viewmodel (default)

I shouldn't have to think in regex to apply what is essentially a single rule for my entire app I think.

There should be a way perhaps of setting these easily in the bootstrapper without so much code?

k

 

Jul 18, 2011 at 11:53 PM
Edited Jul 19, 2011 at 12:04 AM
keithpatton wrote:

Thanks for this. Can I suggest an idea?

for simplicity, it would be nice to express the mapping in a very simple way like:

view root namespace = project.views

Vm root namespace = project.common.viewmodels

view class suffix = view (default)

vm class  suffix = viewmodel (default)

I shouldn't have to think in regex to apply what is essentially a single rule for my entire app I think.

There should be a way perhaps of setting these easily in the bootstrapper without so much code?

k

 

The current transformation mechanism works out-of-the-box without forcing anybody to specify a namespace for the View or ViewModel. The namespace for a given ViewModel is inferred from the namespace hierarchy for a given View, and the namespace for a given View is inferred from the namespace hierarchy for a given ViewModel.

What you're suggesting would force everybody to specify the namespaces explicitly. Think about it. What would be the default, out-of-the-box "root namespaces" for either Views or ViewModels so that the framework wouldn't force you to write code in the bootstrapper?

If you want to avoid writing custom code in the bootstrapper, just follow the standard naming convention and get rid of "Common" from the namespace for your ViewModels or add "Common" to the namespace for your "Views". The framework can go either way, but the namespace of the type being located needs to be inferrable from the namespace of the given type.

Jul 19, 2011 at 2:04 AM

to clarify, I'm just suggesting the simple option as just that, an option, something that would be translated under the hood.

my scenario has a shared library containing more than just view models with multiple phone projects, so don't really want to drop ability to namespace myself.

Thanks

Jul 19, 2011 at 5:54 AM
Edited Jul 19, 2011 at 6:07 AM
keithpatton wrote:

to clarify, I'm just suggesting the simple option as just that, an option, something that would be translated under the hood.

my scenario has a shared library containing more than just view models with multiple phone projects, so don't really want to drop ability to namespace myself.

Thanks


Well, this "simple option" wouldn't be so simple. It would require two ways to do transformations: the standard way based on pattern matching and the more explicit, "simple" method that you're suggesting.

You can make regular expressions as complex or as simple as you want. So you need only write regular expression patterns to be more specific to your use case, and these regular expression patterns will be "simpler" for you to understand. What I posted above was modelled on the built-in rules, which assume a set of strict conventions but allow for great flexibility provided that you follow the conventions. If you have no need to resolve to interface types, that alone will simplify the call to NameTransformer.AddRule() quite a bit. If you know that you'll always have only one namespace each for both the Views and ViewModels, those namespaces can be "hard-coded". If all your Views end with "View" and all your ViewModels end with "ViewModel", that simplifies the regular expression pattern matching even further. Given all of these conditions, your custom rules can be reduced to:

            ViewModelLocator.NameTransformer.AddRule
            (
                @"^Project\.Views\.(?<basename>[A-Za-z_]\w*)View$",
                @"Project.Common.ViewModels.${basename}ViewModel"
            );

            ViewLocator.NameTransformer.AddRule
            (
                @"^Project\.Common\.ViewModels\.(?<basename>[A-Za-z_]\w*)ViewModel$",
                @"Project.Views.${basename}View"
            ); 

These will yield the following transforms:

Project.Views.MainPageView =>Project.Common.ViewModels.MainPageViewModel

Project.Common.ViewModels.MainPageViewModel =>Project.Views.MainPageView

Is that simple enough for you?

Jul 19, 2011 at 11:09 AM

thanks, that looks good, i have a very straightforward mapping and so a few lines like that makes sense.

Jul 19, 2011 at 11:34 AM

just to confirm that works for me, thanks a lot;)

Jul 19, 2011 at 11:36 AM

oh, but i also had to explicitly override SelectAssemblies as well to include the other assemblies as these aren't loaded by default it seems on wp7 at least

 

Coordinator
Jul 19, 2011 at 1:51 PM

By default SelectAssemblies only returns the assembly that the application is defined in.