简体   繁体   中英

Complex WPF UserControl in MVVM application

Having taken over maintenance of an existing WPF application, I was horrified to discover that two of the Views and ViewModels had large blocks of near-identical code. Obviously, I want to refactor this so they can both reuse a single block of functionality, but I'm not sure how best to go about it, architecturally.

The identikit code deals with processing UI data from a tab. However I split this, it is essential that the code in the other tabs (which is different in the two cases) has access to the properties and objects of the tab I need to split out.

To further complicate matters, the replicated code needs database access. We've got a repository object that handles this. Normally when creating new objects, I've been making them testable by passing a copy of the repository into the constructor. However, if I do that in this case I'll have two copies of the repository object - one in the ViewModel, one in the split out code - needing to handle the same data, which is going to cause concurrency issues.

My first thought was to make a UserControl for this, but the more I think about this, the more problematic the two issues above seem to be.

The other option I've considered is just to make a Helper class to do some of the identical processing. But that's only going to partially solve the problem as some identical UI code (raising property changed events, XAML, etc) is still going to be in both Views/ViewModels.

What's the best approach here? Is there a way I can get past the repository/access issues and make a UserControl? Or is than an alternative based on Interfaces or Inheritance I haven't considered?

EDIT - Code was asked for. It's a bit complex to give a comprehensive example, but here's a snippet from each VM:

public void CheckOrderHist(int months)
{
    var endDate = DateTime.Today.AddMonths(months);
    Dictionary<OrderHistory, bool> orders = new Dictionary<OrderHistory, bool>();
    this.ordersToExclude.Clear();

    foreach (var kvp in rep.OrderHistories.GetRecent(months))
    {
        if (kvp.Key.MailingDate >= endDate)
        {
            orders.Add(kvp.Key, true);
            this.ordersToExclude.Add(((OrderHistory)kvp.Key).OrderID);
        }
        else
        {
            orders.Add(kvp.Key, false);
        }
    }

    BuildOrderExclusionsOnCount(); //code in this is near-identical across VM's too
    OrderHistoryMonths = Math.Abs(months); //OrderHistoryMonths is a property on the ViewModel
    OnPropertyChanged("MajorityOrderBoolean");
}

And in the other VM:

private void CheckOrderHist(int months)
{
    var endDate = DateTime.Today.AddMonths(-months);
    ObservableCollection<Tuple<OrderHistory, bool>> orders = new ObservableCollection<Tuple<OrderHistory, bool>>();
    this.ordersToExclude.Clear();

    foreach (var tuple in rep.OrderHistories.GetRecent(-months))
    {
        if (tuple.Item1.MailingDate >= endDate)
        {
            orders.Add(new Tuple<OrderHistory,bool>(tuple.Item1, true));
            this.ordersToExclude.Add(tuple.Item1.OrderID);
        }
        else
        {
            orders.Add(new Tuple<OrderHistory, bool>(tuple.Item1, false));
        }
    }

    BuildOrderExclusionsOnCount(); //code in this is near-identical across VM's too
    OrderHistoryMonths = months; //OrderHistoryMonths is a property on the ViewModel
    OnPropertyChanged("OrderHistories");
    OnPropertyChanged("GroupedOrders");
}

This illustrates the problem nicely - the function is essentially the same, but one uses a Dictionary and the other a Tuple (there's no good reason for this - they both need a Tuple really, for ease of ordering). And one arbitrarily takes a negative int parameter, and the other a positive.

Both contain different OnPropertyChanged events, and will use different copies of the repository object, making it hard to properly separate them using a Helper class. Yet putting it in a UserControl would isolate them from OrderHistoryMonths on the main ViewModel.

If I'm hearing the current comments right, the best solution here is to farm out the main ForEach loop to a helper class, and just put up with the rest of the duplication?

By all means, extract common logic where possible to a new 'helper' class that each ViewModel can construct; this is the standard pattern of re-use through composition. The code you've shown in your question is a good candidate for this kind of refactoring.

As far as boilerplate, though, it's a bit trickier. This is something that is difficult to address in general and must be examined on a case-by-case basis. There are various ways to simplify property changed notification, for instance (helper methods encapsulating property updates, AOP, etc.) but these are generally part of your MVVM framework and embraced application-wide. As far as XAML duplication, you can often use Styles, Data Templates and Value Converters to improve things, but again, it requires a careful analysis of your particular code base to identify the patterns that may merit this treatment. If you have more specific examples that you think are clear duplicates, but aren't sure how to refactor, those may make good questions.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM