user2527902
user2527902

Reputation: 135

WPF: Change databinding while using MVVM

The following is a simplified example of what I'm really trying to do, but my question about it is the same.

Suppose I have two objects, Man and Woman, both of which have the same Properties (Age, Height and Weight), but they are two distinct objects. I cannot change that.

Now suppose that I have a WPF panel, made using the MVVM principle that shows the Age of a certain Man in a textbox. I use Text="{Binding Path=OnePerson.Age}" for that, where OnePerson is an object of the type Man defined in the viewmodel.

This works fine, but I want a similar page to display this information of a Woman. Ideally, I'd like to just use the same view and viewmodel as before. But this is tricky, because the databinding points to the Man-object OnePerson. I can change the databinding programmatically (as described in WPF Binding Programatically), but I can only do so from the view's codebehind. I'm not allowed to do that, because we're using the MVVM model.

I would like to make OnePerson refer to either a Man or Woman object, but I don't know a good way to do so. They are different types, so I can't just assign either a Man or Woman using an if statement. I could declare OnePerson as an object instead of as a type, but then I can't access the Age, Height and Weight properties so easily anymore. Or I could make an entirely different ViewModel, one of which declares OnePerson as a Man and the other as a Woman, and use the same View for both of them. I think that should work, but it seems a bit weird to have two viewmodels for a single view. And adding my own Person class and translating between that and either Man or Woman would probably just make the whole viewmodel considerably more complicated when I start adding functionality like adding a new Man/Woman or editing an existing one, to the point where I might as well copy-paste the Man view and viewmodel and change only the OnePerson object to a Woman.

My question is, is there a clean and simple way to use a single View and Viewmodel to display the information of either a Man or a Woman in this case. Or should I not bother and make seperate pages for these cases?

Hope this is clear enough.

Upvotes: 2

Views: 1877

Answers (3)

Rachel
Rachel

Reputation: 132548

I think this is more of a problem on the ViewModel than the View or bindings. The View is only meant to be a visual representation of the ViewModel, and it sounds like your problem needs to be fixed in the ViewModel, not the View.

WPF's binding system only throws a warning if a binding is invalid, and it doesn't care data type the DataContext is. The binding shown in your question ({Binding OnePerson.Age}) will evaluate correctly regardless of if OnePerson is a Man or a Woman object, and will display the value of any Age property on that object.

Because of that, the best solution would be to make the OnePerson property a type that could be either a Man or a Woman. An interface containing all the shared properties would be ideal because then it's properties can be accessed by the code without casting, and you can keep all the bindings you already have.

IPerson OnePerson { get; set; }

If it's not possible to use a shared interface than you could use an object, however you'd need to remember to cast the OnePerson object to a Man or Woman class before referencing it's properties in code.

object OnePerson { get; set; }

...

if (((Man)OnePerson).Age < 0) ...

A second alternative is to create two separate properties, one for Man and one for Woman. You could then access the properties of whatever item you're working with easily from the ViewModel, and you could use a single View for the ViewModel.

Man SomeMan { get; set; }
Woman SomeWoman { get; set; }

You will probably need some flag to identify which is the populated object though, and to use this flag both for updating the object's properties and when you want to determine the View's DataContext

Here's an example of using a flag to determine the DataContext

<Style x:Key="MyContentControlStyle">
    <Setter Property="Content" Value="{Binding SomeMan}" />
    <Style.Triggers>
        <DataTrigger Property="{Binding SelectedPersonType}" Value="Woman">
            <Setter Property="Content" Value="{Binding SomeWoman}" />
        </DataTrigger>
    </Style.Triggers>
</Style>

And of course, if you do use a separate ViewModel or want separate templates for each object type, you can easily do that using something like implicit DataTemplates to determine how to draw each object.

<DataTemplate DataType="{x:Type myModels:Man}">
    <myViews:ManUserControl />
</DataTemplate>
<DataTemplate DataType="{x:Type myModels:Woman}">
    <myViews:WomanUserControl />
</DataTemplate>

<ContentPresenter Content="{Binding SelectedPerson}" />

Overall I'd recommend you either try to make OnePerson be a data type that is shared by both objects, or you create a separate ViewModel for both objects. It sounds like both your classes are very similar, so you might even be able to get away with some kind of generic ViewModel, such as PersonViewModel<T>, where you pass in Man or Woman as T

Upvotes: 3

blindmeis
blindmeis

Reputation: 22435

if you just want to use one xaml control for your age you can do

ageusercontrol

  <TextBlock Text="{Binding Path=Age}" />

personview

   <local:AgeUserControl DataContext="{Binding Path=MyObjectTypePersonProperty} />

Upvotes: 0

DRapp
DRapp

Reputation: 48139

Have you thought of making the object based on an Interfaced object. The interface basically creates a contract stating that anything derived from it must have whatever it has declared at a minimum... derived controls can have more, but at a minimum the things you want. For example.

public interface IPersonProperties
{
   string PersonName { get; set; }
   int Age { get; set; }

   // if you want a function that is common between them too
   bool SomeCommonFunction(string whateverParms);

   etc...
}

public class Man : IPersonProperties
{
   // these required as to support the IPersonProperties
   public string PersonName { get; set; }
   public int Age { get; set; }

   public bool SomeCommonFunction(string whateverParms)
   {  doSomething;
      return true;
   }


   // you can still have other stuff specific to man class definition
   public string OtherManBasedProperty { get; set;}

   public void SomeManFunction()
   {  // do something specific for man here }
}

public class Woman : IPersonProperties
{
   // these required as to support the IPersonProperties
   public string PersonName { get; set; }
   public int Age { get; set; }

   public bool SomeCommonFunction(string whateverParms)
   {  doSomething;
      return false;
   }

   // you can still have other stuff specific to WOMAN class definition
   public string OtherWOMANBasedProperty { get; set;}

   public void SomeWomanFunction()
   {  // do something specific for man here }

}

Then, in your MVVM, the "object" that you can expose is that of an IPersonProperties such as

public class YourMVVM
{
   public IPersonProperties BindToMe{ get; set }

   public YourMVVM()
   {
      BindToMe = new Man();
      // OR... BindToMe = new Woman();
   }
}

Then, however / where ever in your MVVM object where you create your objects, do so. Your binding would be the same, yet could be of either gender. If something was common between them via the interface, you could reference that simply by

if( BindToMe.SomeCommonFunction( "testing"))
   blah blah.

But, if in some method you need to do something based on a gender specific, you could do...

if( BindToMe is Man )
   ((Man)BindToMe).SomeManFunction();
else
   ((Woman)BindToMe).SomeWOMANFunction();

Hope this opens some doors for you on implementation choices.

Upvotes: 1

Related Questions