WPF/MVVM: Re-use a ViewModel in multiple Controlle

2019-06-14 13:15发布

问题:

In my AdministrationController I use a PupilViewModel like:

_adminRepo.GetSchoolclassPupilList().ForEach(s =>
            {
                SchoolclassViewModel sVM = new SchoolclassViewModel(s, _adminRepo);

                foreach (PupilViewModel pVM in sVM.PupilListViewModel)
                {
                    pVM.Documents.DeleteDocumentDelegate += new Action<List<Document>>(OnDeleteDocument);
                    pVM.Documents.AddDocumentDelegate += new Action(OnAddDocument);
                    pVM.Documents.OpenDocumentDelegate += new Action<int, string>(OnOpenDocument);
                }
                SchoolclassList.Add(sVM);
            }); 

The PupilViewModel is created that way:

public SchoolclassViewModel(Schoolclass schoolclass, IAdministrationRepository adminRepo)
        {
            _schoolclass = schoolclass;
            _adminRepo = adminRepo;  

            PupilListViewModel = new ObservableCollection<PupilViewModel>();
            schoolclass.Pupils.ForEach(p => PupilListViewModel.Add(new PupilViewModel(p, _adminRepo)));                      
        }

As you surely noticed the PupilViewModel takes 2 paramter in its Constructor. The important one is the 2nd paramter which is a service/repository to be specific its the IAdministrationRepository instance.

There exists now another Controller called IncidentReportingController. In its Constructor I do the same as in the AdministrationController:

// When I now try to wrap my pupils into a ViewModel I have a problem:

IEnumerable<Pupil> pupils = incidentRepo.GetPupilIncidentReportDocumentList();         
PupilViewModels = new ObservableCollection<PupilViewModel>(pupils.Select(p => new PupilViewModel(p, ???)));

A.) Either I do not want to pass a Service to the PupilViewModel because there is no reason to update a property i the PupilViewModel as it is readonly in the View.

B.) In my AdministrationController I get data from the service from this Aggregation: 1 Schoolclass has N Pupils and 1 Pupil has N Documents. Those entities are wrapped into SchoolclassViewmodels, PupilViewModels and DocumentViewModels...

Now in my IncidentController I get data from the service too and my Aggregation is pretty similar: 1 Pupil has N IncidentReports and 1 IncidentReport has N Documents. Those entities are wrapped into PupilViewModels, IncidentReportViewModels, DocumentViewModels...

PROBLEM is => In the PupilViewModel class it wraps already a DocumentListViewModel. Now I need the PupilViewModel again to wrap a IncidentReportListViewModel and Later again I have 1 Pupil has 1 SeatingChair and wrap those again. That means I have to pass THREE Services to the PupilViewModel although I do not always need them.

Its hard for me to cut right to the chase with the problem but somehow I feel that is not the right way.

So How do I re-use the same ViewModel which wrap entities with different aggregations having different services?

回答1:

Without knowing how far down the path you are...I would highly suggest you take a look at Prism which makes use of Unity. As stated on Unitys web site...

The Unity Application Block (Unity) is a lightweight extensible dependency injection container with support for constructor, property, and method call injection.

What you gain when making use of these varying frameworks is the extensive decoupling and the 'duty' of instantiation among other things. You then no longer need to worry about the parameters being passed into the constructor as the framework will resolve them for you.

You can also for instance set the lifetime of a registered type, say IDoSomethingController, to an actually type, DoSomethingController...and set it's lifetime to behave as a Singleton should you need the single instance being passed around as someone requests an object of type IDoSomethingController.

Once you make use of these frameworks you are no longer 'newing' up an instance, you are making use of the framework to provide the reuse that you are looking for.

IDoSomethingController controller = IUnityContainer.Resolve<IDoSomethingController>();

EDIT: Since you stated you are using MEFedMVVM; the DI framework exists. Your PupilViewModel is an instance of an ObservableCollection. This is overkill, pass this through. a ViewModel should need to carry additionally weight before you depart simply passing the data through. Your ViewModels seem to be trying to represent objects verusu concepts. What I mean is that you could simply have a SchoolViewModel which exposes pupils, classes, etc.. Those items become Models which can be aggregated in some form on your ViewModel. The ViewModel is meant to be the mediator to the View. It can contain a wealth of information across varying models and services, becoming a single point of data for the View.



回答2:

You seem to have un-necessary use of a repository on your view model? I would expect to see a repository on your business types, not your view model.

For example...

PubilsViewModel(ISchool school) { }

The pupils view model would simply require a school instance, injected by Unity (or your chosen IOC), all of the methods to save, update etc are stored away in the business object.

var school = new School();
school.Save();
school.Update();

or even Static methods on the business object which accept a school type in their constructors? Either way, I would expect your view mode to be calling methods on the Business Objects, it seems more logical for the School object to know the details of how it is saved (including an validation that might need to be done before eventually allowing the save to the database).

On the business object, that is where you would have an instance of the School Repository, the business object doesnt need to know the mechanics of how it is pursisted, thats handed off the repository to worry about, the business object would simply perform any validation of business rules to itself and then make the decision of wether it should save itself to the database or not by calling methods on the repository.

Example:

public class School
{
   private ISchoolRepository _repository;

   public string Name { get; set; }

   public School()
   {
      this._repository = IoC.Resolve<ISchoolRepository>();
   }

   public bool IsValid()
   {
     // Some kind of business logic?
     if (this.Name != null)
     {
       return true;
     }

      return false;
   }

   public void Save()
   {
      if (this.isValid())
      {
         this._repository.Save(this)
      }
}

The repository would handle any code that is needed to save the record to the database.

The School Entity should have no knowledge of HOW to save itself, there is no reason why it cant know the details of what it needs in order to save itself though... the ViewModel does not need to know that level of detail (and shouldnt), using dependency injection, the business object (School) simply knows that it needs an ISchoolRepository, instantiates it and then calls the methods on it.

This would solve your problem...

// Knows it needs a school.
// When needing to start the save process for a School, would call the methods on the
// school instance provided in the constructor.
PupilViewModel(School school) { }

// Knows it needs a Repository.
// Would perform validation of business rules and call methods on the repository when//
// it is ready to be pursisted.
School(ISchoolRepository repository) {}

// Repository
// Would perform the read / write of the data.
SchoolRepository() {}

Does that make sense? Hopefully you can see the seperation of concerns which is what you have started to try and acheive... hope it helps!

Ben



回答3:

I will answer my own question and those who followd my question or answered here please test/check my solution and comment wether its a worth a solution:

The origion problem was I reuse the PupilViewModel at 3 places in different context. One time the PVM`s ctor needs a AdministrationService the other usage it needs a IncidentReportingService and the last time the 3rd service I forgot...

Now injecting ALL 3 services at ALL 3 places seems dumb to me:

PupilViewModel pVM = new PupilViewModel(pupil,adminService,IncidentReportingService,3rdService);

Doing this 3 times looks like bad architecture. Actually I need this: Just wrap the pupil and inject all 3 services WHEN I NEED them really!!!

http://marlongrech.wordpress.com/2010/05/23/mefedmvvm-v1-0-explained/

Scroll down guys and you see MEFedMVVM can do Ctor and Prop injection!!! Totally overlooked that option and its the MEF default injection (Property Injection).

EDIT: That does not work, because of this:

public SchoolclassViewModel(Schoolclass schoolclass, IAdministrationRepository adminRepo)
        {
            _schoolclass = schoolclass;
            _adminRepo = adminRepo;          

            //this.PropertyChangedHandler(object o, PropertyChangedEventArgs e)
            //{
            //    switch (e.PropertyName)
            //    {
            //        case "SchoolclassCode" : _saRepo.UpdateSchoolclass(_schoolclass); break;       
            //        default: break;
            //    }
            //}  

            PupilListViewModel = new ObservableCollection<PupilViewModel>();
            schoolclass.Pupils.ForEach(p => PupilListViewModel.Add(new PupilViewModel(p, _adminRepo)));
        }

        public IAdministrationRepository AdministrationRepository { get; set; }

You see how the _adminRepo is passed to the new created PupilViewModel? At that time when the Ctor is run the Property AdministrationRepository is still null to get the Repo from there... bad :/