Reputation: 669
Can anyone please explain the theory on creating a loosely coupled viewmodel.
I have attached some example code below to try and explain what I mean.
I have 2 example classes just for this example
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Web.UI.Models
{
public class EmployerAddress
{
public string address { get; set; }
public string city { get; set; }
public string region { get; set; }
public string country { get; set; }
public string postZipCode { get; set; }
}
public class EmployerDetails
{
public string position { get; set; }
public string gender { get; set; }
public string dob { get; set; }
}
public class DisplayEmployerAddress : IDisplayEmployerAddress
{
public IEnumerable<EmployerAddress> employerAddr()
{
List<EmployerAddress> Data = new List<EmployerAddress>();
Data.Add(new EmployerAddress
{
address = "address1",
city = "city1",
region = "region1",
country = "country1",
postZipCode = "post zip1"
});
return Data;
}
}
public class DisplayEmployerDetails : IDisplayEmployerDetails
{
public IEnumerable<EmployerDetails> employerDetails()
{
List<EmployerDetails> Data = new List<EmployerDetails>();
Data.Add(new EmployerDetails
{
position = "trainee",
gender = "male",
dob = "22-08-1964"
});
Data.Add(new EmployerDetails
{
position = "trainee2",
gender = "male2",
dob = "22-08-1970"
});
return Data;
}
}
}
The code above has the interfaces:
IEnumerable<EmployerAddress> employerAddr();
IEnumerable<EmployerDetails> employerDetails();
I then use Ninject to bind the above.
kernel.Bind<IDisplayEmployerAddress>().To<DisplayEmployerAddress>().InSingletonScope();
kernel.Bind<IDisplayEmployerDetails>().To<DisplayEmployerDetails>().InSingletonScope();
At this point everything is OK, I could just change DisplayEmployerAddress etc and so long as all the methods etc match up the code will still work.
I then create a viewmodel
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Web.UI.Models
{
public class EmployerDetailsViewModel
{
public string age { get; set; }
public IEnumerable<EmployerAddress> EmployerAddress { get; set; }
public IEnumerable<EmployerDetails> EmployerDetails { get; set; }
}
}
But now this would cause a problem as EmployerAddress is now tightly coupled, so if I change the code, it will now have to be updated in 2 places.
In my controller I have
public class HomeController : Controller
{
private readonly IDisplayEmployerAddress _address;
private readonly IDisplayEmployerDetails _details;
public HomeController(IDisplayEmployerAddress address,
IDisplayEmployerDetails details)
{
_address = address;
_details = details;
}
public ActionResult Index()
{
ViewBag.Title = "Title";
var Address = _address.employerAddr();
var Details = _details.employerDetails().AsEnumerable();
var Age = _details.employerDetails().FirstOrDefault().dob;
var employerModel = new EmployerDetailsViewModel
{
EmployerAddress = Address,
EmployerDetails = Details,
age = age.calAge(Age)
};
return View(employerModel);
}
I keep the controller lightweight as all the books I read say keep as little code as possible in the controller, so to calculate age I use a static class.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Web.UI.Models
{
public static class age
{
public static string calAge(string dob)
{
//Would cal age here
return "48";
}
}
}
So my question is 3 parts.
If I did not want to use a foreach loop how can I get each item out of say EmployerDetails
This employer holds the position of @Model.EmployerDetails.position the gender is @Model.EmployerDetails.gender
<ul>
@foreach (var d in Model.EmployerAddress)
{
<li>@d.address</li>
<li>@d.city</li>
<li>@d.country</li>
<li>@d.region</li>
<li>@d.postZipCode</li>
}
</ul>
<ul>
@foreach (var dd in Model.EmployerDetails)
{
<li>@dd.position</li>
<li>@dd.gender</li>
<li>@dd.dob</li>
}
</ul>
So far
This employer holds the position of @Model.EmployerDetails.position the gender is @Model.EmployerDetails.gender
Worked out question 3, changed code to @Model.EmployerDetails.FirstOrDefault().position
Hope the above example makes sense on what I'm trying to learn
Thanks
George
Upvotes: 0
Views: 833
Reputation: 3661
public class Employer
{
public int Id { get; set; }
}
public class EmployerAddress
{
public string Address { get; set; }
public string City { get; set; }
public string Region { get; set; }
public string Country { get; set; }
public string PostZipCode { get; set; }
public int EmployerId { get; set; }
}
public class EmployerDetails
{
public string Position { get; set; }
public string Gender { get; set; }
public string Dob { get; set; }
public int EmployerId { get; set; }
}
public class MyRepository : IMyRepository
{
public IEnumerable<Employer> GetEmployers()
{
return new List<Employer>
{
new Employer {Id = 1},
new Employer {Id = 2}
};
}
public IEnumerable<EmployerAddress> GetEmployeeAddresses()
{
return new List<EmployerAddress>
{
new EmployerAddress
{
EmployerId = 1,
Address = "address1",
City = "city1",
Region = "region1",
Country = "country1",
PostZipCode = "post zip1"
},
new EmployerAddress
{
EmployerId = 2,
Address = "address2",
City = "city2",
Region = "region2",
Country = "country2",
PostZipCode = "post zip2"
}
};
}
public IEnumerable<EmployerDetails> GetEmployeeDetails()
{
return new List<EmployerDetails>
{
new EmployerDetails
{
EmployerId = 1,
Position = "trainee",
Gender = "male",
Dob = "22-08-1964"
},
new EmployerDetails
{
EmployerId = 2,
Position = "trainee2",
Gender = "male2",
Dob = "22-08-1970"
}
};
}
}
public class EmployerChangedEvent
{
public EmployerChangedEvent(Employer selectedEmployer)
{
Employer = selectedEmployer;
}
public Employer Employer { get; set; }
}
public class EmployerViewModel
{
private readonly IEventAggregator _events;
private Employer _selectedEmployer;
// Configure Ninject properly to get those types
public EmployerViewModel(IEventAggregator events, IMyRepository myRepository)
{
_events = events;
Employers = myRepository.GetEmployers().ToList();
EmployerAddressViewModel = new EmployerAddressViewModel(_events, myRepository);
EmployerDetailsViewModel = new EmployerDetailsViewModel(_events, myRepository);
}
public List<Employer> Employers { get; set; }
public EmployerAddressViewModel EmployerAddressViewModel { get; set; }
public EmployerDetailsViewModel EmployerDetailsViewModel { get; set; }
public Employer SelectedEmployer
{
get { return _selectedEmployer; }
set
{
_selectedEmployer = value;
// this notifies the dependent view models in a loosley coupled way
_events.Publish(new EmployerChangedEvent(_selectedEmployer));
}
}
}
public class EmployerAddressViewModel :
IHandle<EmployerChangedEvent> // specifies which events shall be caught
{
private readonly IMyRepository _myRepository;
private Employer _selectedEmployer;
public EmployerAddressViewModel(IEventAggregator events, IMyRepository myRepository)
{
_myRepository = myRepository;
// this subscribes this view model to the passed event aggregator
// from your main view model (EmployerViewModel)
events.Subscribe(this);
}
public EmployerAddress EmployerAddress { get; set; }
public void Handle(EmployerChangedEvent message)
{
_selectedEmployer = message.Employer;
EmployerAddress = _myRepository.GetEmployeeAddresses()
.FirstOrDefault(e => e.EmployerId == _selectedEmployer.Id);
}
}
public class EmployerDetailsViewModel :
IHandle<EmployerChangedEvent> // specifies which events shall be caught
{
private readonly IMyRepository _myRepository;
private Employer _selectedEmployer;
public EmployerDetailsViewModel(IEventAggregator events, IMyRepository myRepository)
{
_myRepository = myRepository;
// this subscribes this view model to the passed event aggregator
// from your main view model (EmployerViewModel)
events.Subscribe(this);
}
public EmployerDetails EmployerDetails { get; set; }
public void Handle(EmployerChangedEvent message)
{
_selectedEmployer = message.Employer;
EmployerDetails = _myRepository.GetEmployeeDetails()
.FirstOrDefault(e => e.EmployerId == _selectedEmployer.Id);
}
}
internal class Program
{
private static void Main(string[] args)
{
// do this with Ninject
var employerViewModel = new EmployerViewModel(new EventAggregator(), new MyRepository());
// this selection should actually be user input
employerViewModel.SelectedEmployer = employerViewModel.Employers.First();
// select another one
employerViewModel.SelectedEmployer = employerViewModel.Employers.Last();
}
}
As I am not familiar with ASP.NET, my answer doesn't imply any of UI notifications.
I suggest Caliburn.Micro's event aggregator class here, because it solves your coupling problem nicely. This library is worth a look anyway for learning the MVVM pattern.
The IEventAggregator
allows you to subscribe with instance of a class to an instance of the aggregator. If multiple view models share an instance of the event aggregator you can easily send events from one to another in a loosley coupled way.
I refactored your original code, to make it more fitting for the actual MVVM pattern (your first question, let's say this implementaion is more proper). I've added an Employer
class, which is basically the main object. It only has an id. The EmployerDetails
and EmployerAddress
also have a new property EmployerId
which is a reference to the Employer
they belong to.
I have put all the stuff to query data in the MyRepository
class!
For each of those three classes exist three seperate view models and they're only coupled through the event aggregator they share (answers your 2nd question). The EmployerViewModel
manages the main data objects of type Employer
and publishes an event as soon as the selected Employer
changes. The new value is passed into the EmployerChangedEvent
which then is caught by the view models which handle this certain kind of event (IHandle<EmployerChangedEvent
). In their Handle()
implementation the passed employer is put into a private field of the receiving view model.
This is just a console application which simulates user input, though try with putting break points on both of the handle methods, as the SelectedEmployer
changes.
I think some kind of stuff I do in my Main()
method should be done in your controllers. I have to mention that this code is just for showing the benefits of the MVVM pattern, it might be over abstracted in some cases. Also things like querying the repository efficiently are not covered at all!
I think my answer also solves your 3rd question, as I see no foreach
is anymore needed.
Remember to reference Caliburn.Micro
if you'd like to run this code. Just get it through NuGet or download it here.
Upvotes: 1