A reusable Previous/Next Contact Collection ViewModel class

Posted on

Problem

I often use a ViewModel class that contains a collection of Contacts and allows me to iterated through the collection with Previous and Next buttons.

I find that I’ve got duplicate versions of this class all over the place and I’d like to make my code reusable.

How would I go about making the following code reusable (i’ve been going round in circles for a while now):

public class PartiesViewModel : BaseViewModel
{
    const int MaxParties = 20;

    public ObservableCollection<Party> Parties { get; set; }

    public PartiesViewModel(ObservableCollection<Party> parties)
    {
        Parties = parties;
        Party = Parties.FirstOrCreateIfEmpty();

        Party.PropertyChanged += UpdatePartyShortNamesList;
        Parties.CollectionChanged += Parties_CollectionChanged;

        ClearCommand = new RelayCommand(ClearPressed, CanClear);
        NextCommand = new RelayCommand(NextPressed, CanPressNext);
        PrevCommand = new RelayCommand(PrevPressed, CanPressPrev);
    }

    void UpdatePartyShortNamesList(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "ShortName")
            NotifyPropertyChanged("PartyShortNames");
    }
    void Parties_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        NotifyPropertyChanged("PartyNames");
        NotifyPropertyChanged("PartyShortNames");
        NotifyPropertyChanged("Count");
    }

    Party _Party;
    public Party Party
    {
        get { return _Party; }
        set
        {
            _Party = value;
            NotifyPropertyChanged("Party");
            NotifyPropertyChanged("Count");
        }
    }

    public string Count
    {
        get
        {
            var list = Parties.ToList();
            list.RemoveAll(x => x.IsEmpty());
            int current = list.FindIndex(x => x == Party) + 1;
            int count = list.Count();
            if (current == 0)
                return string.Format("{0} of {0}", count + 1);
            else
                return string.Format("{0} of {1}", current, count);
        }
    }

    public RelayCommand PrevCommand { get; set; }
    public bool CanPressPrev() { return IndexOf(Party) > 0; }
    public void PrevPressed()
    {
        try
        {
            int i = IndexOf(Party);
            Party = Parties[i - 1];
        }
        catch (Exception ex) { Message.ShowError(ex); }
    }

    public RelayCommand NextCommand { get; set; }
    public bool CanPressNext() { return !Party.IsNullOrEmpty() && IndexOf(Party) != MaxParties; }
    public void NextPressed()
    {
        try
        {
            if (IndexOf(Party) + 1 == Parties.Count)
            {
                Party = new Party();
                Parties.Add(Party);
            }
            else
            {
                int i = IndexOf(Party);
                Party = Parties[i + 1];
            }
        }
        catch (Exception ex) { Message.ShowError(ex); }
    }

    public RelayCommand ClearCommand { get; set; }
    public bool CanClear()
    {
        return !Party.IsNullOrEmpty();
    }
    public void ClearPressed()
    {
        try
        {
            int i = IndexOf(Party);
            if (Party == Parties.Last())
                Party.Clear();
            else
            {
                Parties.RemoveAll(x => x == Party);
                Party = Parties[i];
            }
        }
        catch (Exception ex) { Message.ShowError(ex); }
    }

    int IndexOf(Party party) { return Parties.ToList().FindIndex(x => x == party); }

    public static List<string> PartyTypes { get { return new List<string>() { "Individual", "Company", "Partnership" }; } }

    public bool IsCompany { get; set; }
}

Solution

A couple of comments:

  • It looks like the class only manages parties. This is nice SRP.
  • Use StyleCop, it makes code much easier to read and review.
  • Validate inputs, makes it much faster to find and fix bugs:

    public PartiesViewModel(ObservableCollection<Party> parties)
    {
        if (parties == null)
        {
            throw new ArgumentNullException(nameof(parties));
        }
        ...
    
  • Party = Parties.FirstOrCreateIfEmpty(); without knowing what this extension method does I don’t love it as it requires me to navigate to another place in the code to see what it does. I guess it does something like this:

    Party = Parties.FirstOrDefault() ?? new Party(...);

    If this is the case I think you should drop the extension method.

  • Party.PropertyChanged += UpdatePartyShortNamesList;

    I can’t see that this subscription is updated anywhere. I would expect the subscription to be removed from the old party and added to the new party in the setter for Party. As it is now it is probably a bug and a memory leak.

  • public ObservableCollection<Party> Parties { get; set; } a public setter here suggests that the collection can be replaced (without notifying the view about it). I assume you want it to be a readonly property. This also applies to the command properties.

    In C#6 you express this as:

    public ObservableCollection<Party> Parties { get; }

    Prior to C#6 this was slightly more verbose:

    private readonly ObservableCollection<Party> _parties;
    
    public PartiesViewModel(ObservableCollection<Party> parties)
    {
        _parties = parties;
    }
    
    public ObservableCollection<Party> Parties
    {
        get { return _parties; }
    }
    

    Being explicit about what is readonly is a very good habit as it makes code easier to reason about and lets the compiler catch more bugs.

  • I have a feeling that binding SelectedIndex can simplify things a lot.

  • The command methods CanPressPrev etc. shall probably be private as the commands are normally the exposed means to interact with the viewmodel.

  • About reuse, this class looks like a nice candidate for composing with. Inject it in other viewmodels where the contact list is needed.

This got long, ending here.

Leave a Reply

Your email address will not be published. Required fields are marked *