Implementing an own “Factory” for reusing Views in

2019-04-01 00:47发布

问题:

I'm currently using WAF (WPF Application Framework) for programming in WPF. I really like the idea to have an own ViewModel for every small View-Unit in my application which I subsequently implemented in this way.

In my project I got a complex list in which every list-element also contains a list. Each list and list-list element is an own ViewModel because of the complexity. The "worst-case" scenario contains 60-90 viewmodels in total, just for the list-view.

(It's a list of questions where each question has a list of answers with ratings and other ui elements).

This implementation works great but the performance is quite bad. After profiling I found out that the error results in creating my ViewModels when I switch between one set of questions (because the whole list has to be generated again).

When I switch between the question-sets I can't reuse my views 1:1 as there are not the same number of questions.

However, I thought I could reuse the given viewmodels and add (in case the new set requires more views) more viewmodels if necessary.

Therefore I've written the following Factory:

[Export]
public class ViewModelPerformanceFactory<T> where T : IPerformanceFactoryViewModel
{
    private List<T> _collection;
    private int _index;
    private readonly ExportFactory<T> _exportFactory;

    [ImportingConstructor]
    public ViewModelPerformanceFactory(ExportFactory<T> exportFactory)
    {
        _exportFactory = exportFactory;
        _index = 0;
    }

    public void Reset()
    {
        _index = 0;
        if (_collection == null)
        {
            return;
        }
        foreach (var elem in _collection)
        {
            elem.Reset();
        }
    }

    public T Get()
    {
        if (_collection == null)
        {
            _collection = new List<T>();
        }
        if (_collection.Count <= _index)
        {
            _collection.Add(_exportFactory.CreateExport().Value);
        }
        return _collection[_index++];
    }
}

where IPerformanceViewModel just offers a Reset-Method to clear the ViewModel and View.

So every time a new question-set is loaded, I call the reset-function of my ViewModelPerformanceFactory which clears all models and set the index back to 0 (so if someone requires a new instance of viewmodel, it will get the first one created).

In theory, this works great.

Now to my question/problem: The more often I switch between my question-sets the slower my application is... It's not the loading of the viewmodel-objects - this is fine. My list just appears very very slow - sometimes even stuck for a few seconds and then continues to build up...

I think this is a WAF-problem as every ViewModel instanciates a View see:

protected ViewModel(TView view) : base(view)
    {
        this.view = view;
    }
}

and it seems like I can't reuse Views as easy as ViewModels in WAF.

Does anyone have a suggestion for me or maybe another approach to speed-up my application? Or does anyone think my whole approach is stupid and I shut stop programming at all? ;)

Edit: There seems to be a memory/performance leak sometimes, but not reproducable every time.. :(

回答1:

Without seeing your entire code, it is difficult to say what your problems are. But guessing from the description you provided:

  1. Absolutely switch to DataTemplates and HierarchicalDataTemplates. If you are creating new controls each time you change data, you will never see stellar performance. This will enable you to use virtualization as well.

  2. The fact that your application is getting slower does indeed indicate a memory leak. Most common cause is events that are not being unsubscribed.

  3. Instantiating ViewModels should not take any significant time given that there are less than 100 of them. If this is the case, you should look for the cause of why they are taking that long. You should not be reusing ViewModels wrapping model objects. If you do, you need a lot of book keeping to 'reset' them, or they have to be stateless, which semi-defeats the purpose of having them in the first place.

  4. The fact that the ViewModel references the View is a major no-go in regards to MVVM. In most of my solutions, I don't even have a view class, I use DataTemplates for everything except custom controls like DateTimeBoxes and custom ComboBoxes.



回答2:

I'm not sure about your approach or why there are performance issues but here is my solution to similar issue.

The full solution can be found https://github.com/steinborge/ProxyTypeHelper/wiki

What I wanted to achieve was the ability to create a 'generic' view model which can then be assigned to a datatemplate. The data template is just a user control. In the case where you have a lot of simple data maintenance screens would save a lot of repetitive code.

But had a couple of issues. Datatemplates don't work well with in XAML with generics and if you have a lot of data templates you are creating a lot of XAML - especially if you wanted to use it in separate views. In your case you mention up to 90 views - this would be a lot of XAML.

Solution is to store the templates in a lookup and use a content control and DataTemplateSelector populate depending upon the DataContext. So first need to register the data template/views:

       manager.RegisterDataTemplate(typeof(GenericViewModel<CarType, WPFEventInter.ViewModel.CarTypeViewModel>), typeof(WPFEventInter.UserControls.CarTypesView));
       manager.RegisterDataTemplate(typeof(GenericViewModel<Colour, WPFEventInter.ViewModel.ColourViewModel>), typeof(WPFEventInter.UserControls.ColourView));

RegisterDataTemplate just adds the datatemplate to a dictionary:

   public void RegisterDataTemplate(Type viewModelType, Type dataTemplateType, string Tag="")
    {
        var template = BuildDataTemplate(viewModelType, dataTemplateType) ;
        templates.Add(viewModelType.ToString() + Tag, template);
    }

    private DataTemplate BuildDataTemplate(Type viewModelType, Type viewType)
    {
        var template = new DataTemplate()
        {
            DataType = viewModelType,
            VisualTree = new FrameworkElementFactory(viewType)
        };

        return template;
    }

Now create a view with a ContentPresenter control .This will display the view depending upon the view's Datacontext.

The DataTemplateSelector looks like the following. This returns the appropriate view depending upon the datacontext:

public class ContentControlGenericTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        DataTemplate retVal = null;
        try
        {
            retVal = Core.WPF.Infrastructure.DataTemplateManager.templates[item.GetType().ToString()];
        }
        catch //empty catch to prevent design time errors..
        {
        }

        return retVal;
    }


回答3:

It is Difficult to provide the solution Without seeing your entire code and what your problems are:

You may us you the code and the substitute the with the your question.

public class MyICommand<T>: ICommand { 
      Action _TargetExecuteMethod; 
      Func<bool> _TargetCanExecuteMethod;

      public MyICommand(Action executeMethod) {
         _TargetExecuteMethod = executeMethod; 
      }

      public MyICommand(Action executeMethod, Func<bool> canExecuteMethod){ 
         _TargetExecuteMethod = executeMethod;
         _TargetCanExecuteMethod = canExecuteMethod; 
      }

      public void RaiseCanExecuteChanged() { 
         CanExecuteChanged(this, EventArgs.Empty); 
      }

      bool ICommand.CanExecute(object parameter) { 

         if (_TargetCanExecuteMethod != null) { 
            return _TargetCanExecuteMethod(); 
         } 

         if (_TargetExecuteMethod != null) { 
            return true; 
         } 

         return false; 
      }

      // Beware - should use weak references if command instance lifetime 
         is longer than lifetime of UI objects that get hooked up to command 

      // Prism commands solve this in their implementation public event 
      EventHandler CanExecuteChanged = delegate { };

      void ICommand.Execute(object parameter) { 
         if (_TargetExecuteMethod != null) {
            _TargetExecuteMethod(); 
         } 
      } 

}