4

I am working on a Windows Phone app that uses MVVM, but am struggling with the implementation of MVVM for properties that need to be formatted from the model class to show in the view.

Let's say that I have a simple model class called Person.

public class Person {

   public string Name { get; set; }
   public DateTime Birthday { get; set; }

}

There is a list of Person objects that are loaded from a locally saved file, and I want to show a list of persons on a list page and then let the user tap on a person to navigate to a details page where there are more details about this person.

On the list page, I want to show the person's birthday as "Birthday: 2/22/1980" (where "2/22/1980" is the person's formatted Birthday)

On the details page, I want to show the person's birthday in a different format: "Eric's birthday is 2/22/1980" (where "Eric" is the person's Name and "2/22/1980" is the person's formatted Birthday).

Normally, I would just create a view model that formats the Birthday properly:

public class PersonViewModel {

   private Person person;

   public PersonViewModel(Person person) {
      this.person = person;
   }

   public string BirthdayForList {
      get {
         return "Birthday: " + person.Birthday.ToString("ddd", CultureInfo.CurrentCulture);
      }
   }

   public string BirthdayForDetails {
      get {
         return person.Name + "'s birthday is " + person.Birthday.ToString("ddd", CultureInfo.CurrentCulture);
      }
   }

}

In order to show these values in the UI, I would create a collection of these view model objects (and bind them to the view):

ObservableCollection<PersonViewModel> Items

Now, what do I do when a person's birthday is updated (somewhere on the details page) and make sure that Items has been updated with up-to-date BirthdayForList and BirthdayForDetails properties, while at the same time saving the Person locally?

I want to keep everything simple and not have to update both the saved list of Person objects and list of PersonViewModel objects manually each time a value needs to be updated.

What is the best way to do this? Should I be using an ObservableCollection of PersonViewModel objects? Also, I have read in several places on this website that the model class should not implement NotifyPropertyChanged.

(Note: I have simplified the problem for this question. You should assume that there are many other ways that I need to format the Birthday property throughout the application as well as other properties from the model class that need to be formatted differently on different pages.)

epaps
  • 516
  • 1
  • 8
  • 20

6 Answers6

5

Why don't simply do the whole thing in xaml and don't use the "calculated properties"?

<TextBlock>
    <TextBlock.Text>
        <MultiBinding StringFormat="{}{0}'s birthday is {1:ddd}">
            <Binding Path="Person.Name">
            <Binding Path="Person.BirthDay">
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

Then all you need to do is implement INotifyPropertyChanged in the Person class and raise the event in the setter.

EDIT: I would also recommend using a framework like MVVM light so you can use the ViewModel and ObservableObject base classes for your objects and simply be able to use their implementation of INotifyPropertyChanged

public string FirstName
{
    get { return _firstName; }
    set
    {
        _firstName = value;
        RaisePropertyChanged(() => FirstName);
    }
}
private string _firstName;
Staeff
  • 4,994
  • 6
  • 34
  • 58
  • It looks like Windows Phone does not support `MultiBinding`. Any ideas? – epaps Mar 04 '14 at 04:29
  • Without having tried it, you could use one of those solutions, I think you should be able to write A) a more generic format converter or B) Apply the StringFormat to one of the "Sub"-Bindings http://www.scottlogic.com/blog/2010/05/10/silverlight-multibinding-solution-for-silverlight-4.html http://www.codeproject.com/Articles/286171/MultiBinding-in-Silverlight-5 – Staeff Mar 04 '14 at 09:54
  • Or use a calculated property but include `RaisePropertyChanged(() => BirthDayForList)` in both setters – Staeff Mar 04 '14 at 09:57
2

Converters and XAML formatting are good solutions when they work, but sometimes you reall just need to do it in the ViewModel. Typically, you'd need to implement INotifyPropertyChanged and raise the PropertyChanged event for the calculated property when any of its dependencies change.

Managing these dependencies is a royal pain in the ... In fact I got so fed up with this very problem that I an MVVM framework called Catwalk that allows you to do these types of calculated properties in your ViewModel. If you use the framework, you can have code like

  public string BirthdayForDetails
  {
    get
    {
      return Calculated(() => this.Name + "'s birthday is " + this.Birthday.ToString("ddd", CultureInfo.CurrentCulture));
    }
  }

Where the base class for model will automatically raise a PropertyChanged event for BirthdayForDetails if either Name or Birthday change. You just have to inherit from ObservableModel and Birthday & Name have to be observable properties like

  public string Name
  {
    get { return GetValue<string>(); }
    set { SetValue(value); }
  }

If you decide to try it out, let me know what you think.

Matt Dotson
  • 5,887
  • 1
  • 23
  • 23
  • Is `Name` in the model and `BirthdayForDetails` in the view model? Wouldn't I still need to manage a list of `Person` view models? – epaps Mar 04 '14 at 04:33
0

You have two options:

  1. Just format the date in XAML. Here is an example.

  2. For more complex conversions, you can use converters.

What you should NOT do is store the format in the view model. The format of the data is a view/presentation concern only. So, the benefit of the above approach is that you don't need to keep separate lists just because of formatting.

Community
  • 1
  • 1
Bob Horn
  • 33,387
  • 34
  • 113
  • 219
  • Thanks for the answer, Bob. For the first option, how would I achieve "[`User`]'s birthday is [`Birthday`]"? I would want to use a single `TextBlock` for this. For the second option, I'm concerned with how well this would scale. I would need to create probably dozens of converters. The actual app is more complex than the example above with many more properties that would need to be formatted. Also, why would I not want to store the format in the view model? – epaps Feb 23 '14 at 19:33
  • You can achieve the 1st option by using MultiBinding. However, this is not available on WP. Use the MultiBindingBehavior of the Cimbalino Windows Phone Toolkit to achieve the same result. http://www.pedrolamas.com/2013/05/17/cimbalino-windows-phone-toolkit-multibindingbehavior/ – Sacha Bruttin Feb 24 '14 at 14:17
0

Putting all your PersonViewModels in an ObservableCollection only solves the issue that your UI needs to update when a new PersonViewModel is added / removed from the collection.

That however does not solve the problem, that one object inside your collection changes. So if the Birthdate of the first person in the list changes, the collection stays the same.

So what you need to achive is to notify the UI that one object inside this collection changed. You can do so either by letting your ViewModel implement INotifyPropertyChanged or deriving it from DependencyObject (discussion on what's the better solution: INotifyPropertyChanged vs. DependencyProperty in ViewModel).

I'd recommend using INotifyPropertyChanged. Implementing that interface will give you an PropertyChanged event. You need to raise that event everytime one of your properties change. Unfortunately this also requires you to create additional properties in the ViewModel so that you get notified when the changes happen.

The simplest (definitly not the best) way would be to just call OnPropertyChanged for every property that is dependent.

public class PersonViewModel : INotifyPropertyChanged
{
    private Person person;

    public PersonViewModel(Person person)
    {
        this.person = person;
    }

    public DateTime Birthday
    {
        get { return person.Birthday; }
        set 
        { 
            person.Birthday = value;
            OnPropertyChanged("Birthday");
            OnPropertyChanged("BirthdayForList");
            OnPropertyChanged("BirthdayForDetails");
        }
    }

    public string Name
    {
        get { return person.Name; }
        set 
        { 
            person.Name = value; 
            OnPropertyChanged("Name");
            OnPropertyChanged("BirthdayForDetails");
        }
    }

    public string BirthdayForList
    {
        get
        {
            return "Birthday: " + Birthday.ToString("ddd", CultureInfo.CurrentCulture);
        }
    }

    public string BirthdayForDetails
    {
        get
        {
            return Name + "'s birthday is " + Birthday.ToString("ddd", CultureInfo.CurrentCulture);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

I already mentioned that this solution is not the best. You are very likely to add another dependent property and you would have to remember adding the OnPropertyChanged to the Property you depend on.

So you could either use converters for your ViewModel (that now notifies on property change) and remove your calculated properties, or you stick to your properties and find an easier way to mark dependent properties.

Thankfully somebody allready solved the issue of dependent / calculated properties. When googeling "INotifyPropertyChanged dependent properties" a lot of results show up. One that I really like is this one (Handling INotifyPropertyChanged for dependant properties) because it uses clean and readable attributes to mark the dependencies.

Also there are several MVVM Frameworks out there, that include solutions for said problem.

I hope one of the suggested solutions does help you fix your problem.

Community
  • 1
  • 1
Robert
  • 105
  • 1
  • 9
  • I just had a look at Matts answer and the Catwalk framework also seems to provide a pretty elegant solution. – Robert Feb 25 '14 at 12:47
0

You can simply call the PropertyChanged method in the setter of your "person" property

like this

private Person myPerson;
public Person MyPerson
{
    get { return myPerson; }
    set
    {
        myPerson = value;
        PropertyChanged("MyPerson");
        PropertyChanged("BirthdayForList");
        PropertyChanged("BirthdayForDetails");
    }
}
Luca Corradi
  • 2,031
  • 1
  • 16
  • 17
0

You can use a combination of multiple elements embedded inside a TextBlock element

<TextBlock Foreground="DarkGray" VerticalAlignment="Bottom" Margin="0,0,0,8"><Run Text="total length "/><Run Text="{Binding TotalHours}" FontSize="48"/><Run Text="h "/><Run Text=":" FontSize="48"/><Run Text="{Binding TotalMinutes}" FontSize="48"/><Run Text="m "/></TextBlock>

Somewhat like this sample https://stackoverflow.com/a/8130843/3214300

Community
  • 1
  • 1
Neeraj
  • 596
  • 7
  • 9