So lets say I have two models:
Thingy and Status. Thingy
has a Status
, and Status has many Thingies. It's typical "Object and Object ty开发者_如何转开发pe relationship".
I have a view where I just want the number of thingies in each status. Or basically a list of Status.Name and Status.Thingies.Count. I could do exactly this, but is the "right" thing to do to create a view model in the form:
ThingiesByStatusViewModel
-StatusName
-StatusThingiesCount
and hook it up with something like AutoMapper.
For such a trivial example, it probably doesn't make much of a difference, but it would help me understand better the proper 'separation of concerns'.
Should I use a viewmodel here?
Is this a rhetorical question?
Your view model would look exactly as you propose and it is perfectly adapted to what you are trying to display here:
public class ThingiesByStatusViewModel
{
public string StatusName { get; set; }
public int StatusThingiesCount { get; set; }
}
and then your controller would return an IEnumerable<ThingiesByStatusViewModel>
. Then in your view you could simply use a display template:
@Html.DisplayForModel()
and the corresponding display template (~/Views/Shared/DisplayTemplates/ThingiesByStatusViewModel.cshtml
):
@model AppName.Models.ThingiesByStatusViewModel
<div>
<span>StatusName: @Model.StatusName</span>
<span>Number of thingies: @Model.StatusThingiesCount</span>
</div>
Now let's look at the mapping layer. Suppose that we have the following domain:
public class Thingy
{ }
public class Status
{
public string StatusName { get; set; }
public IEnumerable<Thingy> Thingies { get; set; }
}
and we have an instance of IEnumerable<Status>
.
The mapping definition could look like this:
Mapper
.CreateMap<Status, ThingiesByStatusViewModel>()
.ForMember(
dest => dest.StatusThingiesCount,
opt => opt.MapFrom(src => src.Thingies.Count())
);
and finally the controller action would simply be:
public ActionResult Foo()
{
IEnumerable<Status> statuses = _repository.GetStatuses();
IEnumerable<ThingiesByStatusViewModel> statusesVM = Mapper.Map<IEnumerable<Status>, IEnumerable<ThingiesByStatusViewModel>>(statuses);
return View(statusesVM);
}
I, personally, don't like to send non-trivial types to the view because then the person designing the view might feel obligated to start stuffing business logic into the view, that that's bad news.
In your scenario, I'd add a StatusName property to your view model and enjoy success.
Yes, you should use VM.
Guess, You have time for some experiments, so try to do it in a proper way. Next time, you will do that much quicker.
If current example is trivial - then it will be easier to get practice session.
Also, In future your application will grow up and you never know when you need to extend it. So implementing it in proper way provide you a good maintainability for future.
The answer is yes. Of course. Why not? It will only involve about 1% of your code!
Think about it:
The purpose of a ViewModel is as a container to send data to the view.
As such, it can "shape" the data sent to the view, that may not correspond to your Domain Model. EG, if Thingies has 50 properties (or columns in a table...), you may only need 3 of those properties.
To provide this shaped data, I use a "Service" class. EG, StatusService (tied down by an interface to allow DI, eg, IStatusService). So the Service class gets instances of your repositories, provides methods to use in your controllers and special read only properties that build your ViewModels for packing the data for the views.
Using this way of doing things, you can easily see that the effort that goes into writing a ViewModel is risible. In terms of lines of code, probably 1 percent.
Want proof?
Look at the following:
A typical Controller would be:
The Controller:
//NOTE THE USE OF: service.ViewModel
namespace ES.eLearningFE.Areas.Admin.Controllers
{
public partial class StepEditorController : Controller
{
IStepEditorService service;
public StepEditorController(IStepEditorService service)
{
this.service = service;
}
[HttpGet]
public virtual ActionResult List(int IdCourse)
{
service.CourseId = IdCourse;
return View(service.Steps());
}
[HttpGet]
public virtual ActionResult Edit(int IdCourse, int IdStep)
{
service.CourseId = IdCourse;
service.CurrentStepId = IdStep;
return View(service.ViewModel);
}
[HttpPost]
public virtual ActionResult Edit(CourseStep step)
{
service.CourseId = step.CourseId;
service.CurrentStepId = step.CourseStepId;
service.CourseId = step.CourseId;
try
{
UpdateModel(service.CurrentStep);
service.Save();
return RedirectToAction(Actions.Edit(step.CourseId, step.CourseStepId));
}
catch
{
// Refactor notice : empty catch block : Return errors!
}
return View(service.ViewModel);
}
[HttpGet]
public virtual ActionResult New(int IdCourse)
{
service.CourseId = IdCourse;
return View(service.ViewModel);
}
[HttpPost]
public virtual ActionResult New(CourseStep step)
{
service.CourseId = step.CourseId;
if (ModelState.IsValid)
{
service.AddStep(step);
try
{
service.Save();
service.CurrentStepId = step.CourseStepId;
return View(Views.Edit, service.ViewModel);
}
catch
{
// Refactor notice : empty catch block : Return errors!
}
}
return View(service.ViewModel);
}
}
}
The Service:
The Service class would look like:
// NOTE THE FOLLOWING PROPERTY: public StepEditorVM ViewModel
namespace ES.eLearning.Domain.Services.Admin
{
public class SqlStepEditorService : IStepEditorService
{
DataContext db;
public SqlStepEditorService(DbDataContextFactory contextFactory)
{
db = contextFactory.Make();
CoursesRepository = new SqlRepository<Course>(db);
StepsRepository = new SqlRepository<CourseStep>(db);
}
#region IStepEditorService Members
public StepEditorVM ViewModel
{
get
{
if (CurrentStep != null)
{
return new StepEditorVM
{
CurrentStep = this.CurrentStep,
Steps = this.Steps()
};
}
else // New Step
{
return new StepEditorVM
{
CurrentStep = new CourseStep(),
Steps = this.Steps()
};
}
}
}
public CourseStep CurrentStep
{
get
{
return FindStep(CurrentStepId, CourseId);
}
}
// Refactor notice : Expose Steps with a CourseId parameter, instead of reading from the CourseId property?
public List<CourseStep> Steps()
{
if (CourseId == null) throw new ApplicationException("Cannot get Steps [CourseId == null]");
return (from cs in StepsRepository.Query where cs.CourseId == CourseId select cs).ToList();
}
// Refactor notice : Pattern for dealing with null input parameters
public int ? CourseId { get; set; }
public int ? CurrentStepId { get; set; }
public CourseStep FindStep(int ? StepId, int ? CourseId)
{
// Refactor notice : Pattern for dealing with null input parameters
if (CourseId == null) throw new ApplicationException("Cannot Find Step [CourseId == null]");
if (CurrentStepId == null) throw new ApplicationException("Cannot Find Step [StepId == null]");
try
{
return (from cs in StepsRepository.Query where ((cs.CourseStepId == StepId) && (cs.CourseId == CourseId)) select cs).First();
}
catch
{
return null;
}
}
public void AddStep(CourseStep step)
{
StepsRepository.Add(step);
}
public void DeleteStep(CourseStep step)
{
StepsRepository.Delete(step);
}
public void Clear()
{
CurrentStepId = null;
CourseId = null;
}
public void Save()
{
db.SubmitChanges();
}
#endregion
#region Repositories
private IRepository<Course> CoursesRepository
{
get;
set;
}
private IRepository<CourseStep> StepsRepository
{
get;
set;
}
#endregion
}
}
The Inteface:
And the interface would look like:
namespace ES.eLearning.Domain.Services.Interfaces
{
public interface IStepEditorService
{
StepEditorVM ViewModel { get; }
CourseStep CurrentStep { get; }
List<CourseStep> Steps();
int ? CourseId { get; set; }
int ? CurrentStepId { get; set; }
CourseStep FindStep(int ? StepId, int ? CourseId);
void AddStep(CourseStep step);
void DeleteStep(CourseStep step);
void Clear();
void Save();
}
}
The ViewModel class:
And, finally, the ViewModel class itself:
namespace ES.eLearning.Domain.ViewModels
{
public class StepEditorVM
{
public CourseStep CurrentStep { get; set; }
public List<CourseStep> Steps { get; set; }
}
}
By comparison with all the rest, it is nothing.
So why not do it?
Other bits:
The Generic Repository:
namespace ES.eLearning.Domain
{
public class SqlRepository<T> : IRepository<T> where T : class
{
DataContext db;
public SqlRepository(DataContext db)
{
this.db = db;
}
#region IRepository<T> Members
public IQueryable<T> Query
{
get { return db.GetTable<T>(); }
}
public List<T> FetchAll()
{
return Query.ToList();
}
public void Add(T entity)
{
db.GetTable<T>().InsertOnSubmit(entity);
}
public void Delete(T entity)
{
db.GetTable<T>().DeleteOnSubmit(entity);
}
public void Save()
{
db.SubmitChanges();
}
#endregion
}
}
IRepository:
namespace Wingspan.Web.Mvc
{
public interface IRepository<TEntity> where TEntity : class
{
List<TEntity> FetchAll();
IQueryable<TEntity> Query {get;}
void Add(TEntity entity);
void Delete(TEntity entity);
void Save();
}
}
NOTE: This is what I am working on now, so it is a work in progress and alot simpler than the final thing will be, but this is certainly the first and second iteration, and it gives an idea of how structured your work can be.
Extra complexity will creep in when the client wants new features in the views etc. But even so, this is a framework you can build on and test the changes very easily.
The reason for doing things this way, imo, is largely to provide a structured way of writing your code. You can have the whole thing written up in a morning, before you have even created the corresponding Views.
Ie, it all goes very quickly and you know exactly what you are trying to do.
Once you have done that, you create your Views and see what happens...
Joking appart, the beauty is that by the time you get to the views, you know what you are doing, you know your data, its shape, and the view design just flows. You then add the extras that the Views demands and hey presto, job done.
Of course, the other reason is testing. But even here, you benefit from a highly structured approach: your tests will follow a very distinct pattern too. So easy to write.
The whole point:
The whole point of the above is to highlight how little effort goes into writing the ViewModel, by comparison with the overall effort.
精彩评论