How to tag viewmodel when using ViewModelLocator?

2019-07-29 09:14发布

I have multiple instances of a viewmodel:

<views:MyView x:Name="view1" mefed:ViewModelLocator.NonSharedViewModel="MyViewModel" />
<views:MyView x:Name="view2" mefed:ViewModelLocator.NonSharedViewModel="MyViewModel" />

These two instances should listen for different messages. Hence, I need to somehow tag these viewmodel instances. How?

I'm using MEFedMVVM and Prism. There ought to be a way to let the viewmodel know about some state. E.g.:

<views:MyView x:Name="view1" mefed:ViewModelLocator.NonSharedViewModel="MyViewModel">
  <!-- Let the viewmodel know it is of type X -->
</views:MyView>
<views:MyView x:Name="view2" mefed:ViewModelLocator.NonSharedViewModel="MyViewModel">
  <!-- Let the viewmodel know it is of type Y -->
</views:MyView>

How can this be achieved?

In an ideal world you would use a view's parameterized constructor through XAML, but this is not supported. Another idea is to use different classes for the views, but that will soon bloat the code!

1条回答
你好瞎i
2楼-- · 2019-07-29 09:58

Quick Answer and Summary:

As you noticed, what you really want is to parameterize your ViewModel from XAML. So my instinct is to write an Attached Behavior to provide the ability to pass in parameters for your ViewModel. And more specifically, what comes to mind is that we want a single Attached Behavior class that we can specify both (a) which ViewModel we want and (b) a parameter for that ViewModel. To satisfy both of these desires in a single class while also being as DRY as possible, I think it is easiest to use a "Blend" Behavior, since Blend behaviors are not static classes and thus it seems much easier to be able to use them to pass in both pieces of related information together.

Explanation:

First a quick disclaimer: I have not used MEFedMVVM or Prism (but I have used other MVVM libraries), so my answer uses a more general approach that I have recently learned to make use of. Thus, this approach does not rely on any "magic" stuff that Prism may give you when you use things in their "normal" way (i.e., hooking up DataContext automatically, etc.), so let that frame this mindset.

For a good writeup about the differences between "regular" Attached Behaviors and "Blend" Behaviors, I like this blog post. I won't repeat his explanations here, but the key thing I notice is that regular Attached Behaviors seem to just rely on a single piece of information (i.e., a single "parameter") in order to do its thing. Such as (from the blog post):

<GridView local:ItemClickNavBehavior.Destination="Home" ...>

Now lets put that in terms of your case, and examine how it could work if you just used regular Attached Bahaviors. You would write an Attached Behavior class, call it "MyViewModel1Creator" that would: (1) register an attached property called "Type" and (2) include a change callback handler for "Type" (which also gets called when its initially set - see the "HookupBehavior" method in the linked blog post). In this change callback you would instantiate "ViewModel1" and pass in to it the value of the "Type" Attached Property. Also in this method you could take care of any other necessities such as setting the DataContext for the View, etc. You can access the object that the Attached Property is attached to (the the View object in this case) by using the first parameter in the callback handler (the Dependency object param).

Then your Xaml use of the "MyViewModel1Creator" class would look like:

<views:MyView x:Name="view1" MyBehaviors:MyViewModel1Creator.Type="X" />
<views:MyView x:Name="view2" MyBehaviors:MyViewModel1Creator.Type="Y" />

Although this would work, I see a disadvantage to this approach (using regular Attached Properties). To use this approach, you would have to create a separate Attached Behavior class for each ViewModel, meaning that if you had 3 ViewModels ("ViewModel1", "ViewModel2", "ViewModel3") then you would need to write 3 Attached Behavior classes ("ViewModel1Creator", "ViewModel2Creator", "ViewModel3Creator"). Each one would instantiate its respective ViewModel (and expose a "Type" Attached Property as shown above). Another disadvantage is that it seems to be harder to find a way to add additional parameters to pass in.

A slightly alternate approach to the one above, but equally deficient in terms of being DRY, would be to have a single class (call it "MyViewModelCreator" - without the "1" this time) that houses several Attached Properties with names like "CreateViewModel_1_WithType", "CreateViewModel_2_WithType", "CreateViewModel_3_WithType", etc. Its usage would look like:

<views:MyView x:Name="view1"
              MyBehaviors:MyViewModelCreator.CreateViewModel_1_WithType="X" />
<views:MyView x:Name="view2" 
              MyBehaviors:MyViewModelCreator.CreateViewModel_1_WithType="Y" />

Again, these approaches are not very DRY, so we really need to...

Now let's consider how it could work if we used a "Blend" behavior:

You would write a class that derives from a typed Behavior - for your views it would probably be Behavior<UserControl>, so your class heading might look like:
public class ViewModelSetupBehavior : Behavior<UserControl>. In this class you would: (1) register as many Dependency Properties as you like, including a "Type" Dependency Property, and a "ViewModelName" Dependency Property, and (2) You will override the OnAttached() method, where you will instantiate whichever ViewModel is indicated by the value of the "ViewModelName" Dependency Property, and also pass in to it the value of the "Type" Dependency Property. Again, this would be the place to also handle any other necessities such as setting the DataContext for the View, etc. You can access the object that the Behavior is "attached to" (your View in this case) by using the AssociatedObject property.

This could let you do this:

<views:MyView x:Name="view1">
    <i:Interaction.Behaviors>
        <MyBehaviors:ViewModelSetupBehavior ViewModelName="ViewModel1" Type="X" SomeOtherParam="bla" />
    </i:Interaction.Behaviors>
</views:MyView>

<views:MyView x:Name="view2">
    <i:Interaction.Behaviors>
        <MyBehaviors:ViewModelSetupBehavior ViewModelName="ViewModel1" Type="Y" />
    </i:Interaction.Behaviors>
</views:MyView>

Now notice that we have the ability to use a single Behavior class for the creation of all of our ViewModels, and we can pass in several parameters to dictate how we want the ViewModel to be instantiated.

查看更多
登录 后发表回答