Recently, I found myself copying views around in ASP.NET MVC application and changing nothing or just a few things, generally name of controller on BeginForm("actionName", "controllerName") (and thankfully, this can be solved inside view). Shortly, my main purpose is to eliminate duplicate views.
Reason
The reason for this is that I have different controllers with similar functionality. They seem same to user but calculations are very different in controllers. More specifically I have four controllers called InitialReport, CorrectionReport, InitialReportReview, CorrectionReportReview. The views of InitialReport, CorrectionReport and InitialReportReview, CorrectionReportReview are almost identical. So what I want to do is to eliminate duplicate views and create shared views for couples.
Problem
My problem is that, I do not want to put all views to the Shared view folder, because they are too many to put to this folder. Additionally, names of actions in controllers overlaps. Say I have ReportPayments action method in all four controllers, so name of views also overlaps. But the view must be different for InitialReport and InitialReportReview for example. So I have added two folders to Shared view folder called Report (for InitialReport, CorrectionReport) and ReportReview (for InitialReportReview, CorrectionReportReview). I plan to add all the shared views to Report and ReportReview sub-folders respectively. Now the problem is that, view look up locations must dynamically change to suit "controller type". For example, if I'm in InitialReport controller and I am navigation to ReportPayments action, then the view must be loaded from ~/Views/Shared/Report/ReportPayments.cshtml, but if I'm in InitialReportReview controller, then the view should be loaded from ~/Views/Shared/ReportReview/ReportPayments.cshtml.
Solution
As a solution I have created custom view engine, I override FindView method and set the ViewLocationFormats according to controller name.
public class ExtendedRazorViewEngine
: RazorViewEngine
{
string[] DefaultViewLocations { get; set; }
string[] ReportViewLocations { get; set; }
string[] ReportReviewViewLocations { get; set; }
public ExtendedRazorViewEngine()
{
// Get the copy of default view locations
DefaultViewLocations = new string[ViewLocationFormats.Length];
ViewLocationFormats.CopyTo(DefaultViewLocations, 0);
// Initialize ReportViewLocations
List<string> customReportViewLocations = new List<string>
{
"~/Views/Shared/Report/{0}.cshtml"
};
ReportViewLocations = DefaultViewLocations
.Union(customReportViewLocations)
.ToArray();
// Initialize ReportReviewViewLocations
List<string> customReportReviewViewLocations = new List<string>
{
"~/Views/Shared/ReportReview/{0}.cshtml"
};
ReportReviewViewLocations = DefaultViewLocations
.Union(customReportReviewViewLocations)
.ToArray();
}
public override ViewEngineResult FindView(
ControllerContext controllerContext,
string viewName,
string masterName,
bool useCache)
{
// Get controller name
string controllerName = controllerContext.RouteData.GetRequiredString("controller");
// Set view search locations
if (controllerName.EndsWith("ReportReview"))
ViewLocationFormats = ReportReviewViewLocations;
else if (controllerName.EndsWith("Report"))
ViewLocationFormats = ReportViewLocations;
else
ViewLocationFormats = DefaultViewLocations;
return base.FindView(controllerContext, viewName, masterName, useCache);
}
}
I have registered ExtendedRazorViewEngine as default view engine and it seems to work fine. But I have few questions. There is a question and answer which are similar (not identical) to mine, and quite helpful indeed, but it does not answer to my questions.
Questions
Is this correct way? Does this method have any drawback such as views are not cached or any other performance issues? Can I make this method any better?
Is there any better way of achieving my goal?
When I tested, I realized that FindView method is hit twice. Why? I copied view engine and tested it in a almost empty MVC application, same thing happened there also, so I do not think it is related to my application.
I think you are making this harder than it needs to be. Putting the View in the Shared folder seems to be much simpler. But if you don't want to do that, you can still call a View even if it's not in the Shared folder.
From a view:
#Html.Partial("~/Views/ReportReview/InitialReport.cshtml")
Or from a Controller:
return this.View("~/Views/ReportReview/InitialReport.cshtml");
So if I were you, I would create these Views once, and just call that one from the other Views or Controllers. This way you don't have to duplicate code, and you don't have to create a custom view engine (which I imagine will be a bear to maintain over the years).
Related
I'm still learning the MVC way of doing web development.
I have a partial view that renders information for a single photo (picture, username, f-stop, other info)
I have several pages where I want to display lists of photos. For example, on my homepage, I want to display the most recent photos to be added to the site.
My current approach to doing this is that I have added a GetNewestPhotos() function to my PhotoController that goes to the database to get the most recent photo records, and, for each one, renders the partialview and concatenates it to the result string (using the nasty-looking RenderPartialViewToString found here). Then client side, I request this string via AJAX and populate a div with the result.
I'm almost sure that this is wrong. How should I do it?
In your controller method, return a partial view and inject your compound object into the view
public class CompoundType
{
public List<Photo> Photos { get; set; }
}
public ActionResult GetNewestPhotos()
{
CompoundType model = provider.GetPhotosFromDbAsCompoundObject();
return PartialView("ViewName", model);
}
In your view, specify that your view's model should be your compound object.
#model CompoundType
At that point, simply iterate over the properties and/or collections in your object to render them into html.
#foreach (var photo in Model.Photos)
{
#Html.Raw(photo.Name).
...
}
There are a number of reasons why this is preferable over your current approach.
Razor gives you strong typing. You can see what your types are and you get vastly more useful runtime exceptions, allowing you to troubleshoot issues more easily.
In your current paradigm, you are actually doing work twice. You are creating the partial views, but then you are taking them and splicing them together on the client. This is redundant work.
Maintainability. Other devs expect to see the pattern I've outlined. By being consistent, you'll find more useful information online and be able to solve problems more quickly when you encounter them. In addition, you can more easily hand over your project with less knowledge transfer.
You have a view model for the page that contains a list of the photo view models. This page view model contains a list of viewmodels for the photos.
In the View for the main page call:
#Html.DisplayFor(m => m.PhotosView)
This will render each view model using the default view you defined.
edit
class MainPageController
{
ActionResult Index()
{
var model = new MainPageViewModel
{
Photos = GetListOfPhotoViewModelsOrderedByAge(SomeDataSource),
}
return View(model)
}
class MainPageViewModel
{
// various other properties
IList<PhotoViewModels> Photos {get; set;}
}
class PhotoViewModel
{
// properties to display about the photo (including hte path to the actual image)
}
The Razor views (mainpage)
#model MainPageViewModel
#Html.DisplayFor(m =>m.Photos)
#* other things on the page *#
Photo view (in the shared/display directory)
#model PhotoViewModel
<img url="#Model.PathToImage" />
I haven't tried this and it's mostly from ther top of my head, there may be slight syntax errors.
Being rather new to ASP.NET MVC, I am already seeing some benefits of it for code reuse and maintainability. When asking a previous question on routing, I had another idea for my example, but unsure how to implement it.
Question: How can I reuse my issue view and controller in separate pages, as well as having parameters for certain things (like how many issues to display)?
Example: Say in my web application I want to show a list of issues. These issues are grouped by projects. So if you go to www.example.com/projectname/issues, you would see a list of issues for that project, and if you went to www.example.com/issues, you would see all issues for all projects. What I would like to do is that if you go to www.example.com/projectname, you would see some info about that project, including the last 10 issues submitted.
How can I reuse this issue code? I see I have the option for Partial View, but when I implemented it, I was unsure how to route any code to it. In Web Forms, you could make a ASP.net control, set some parameters in the code behind, and then embed this control elsewhere.
Some of the examples I have found so far either lack a complete implementation (goiing beyond just adding some HTMl in other pages), look like older MVC code that doesn't seem to work for me in ASP.NET MVC 3, and lack allowing me to set paramaters and showing this type of reuse.
My terminology may not be entirely correct here. If anything, I am trying to find the best (read MVC) way to replicate something like ASP.net Web Forms User Controls. As in, reusing my 'issues' code (HTML and C#) on both a 'master' issues list, as well as an issues 'widget' if you will
Skip the temptation write code in the view that goes out and accesses data on it's own. That includes using built-in functions like RenderAction. Even though RenderAction "goes back" to execute another controller it doesn't mean the view isn't taking an action on its own, which arguably breaks the MVC approach where views are supposed to do nothing and the model is supposed to contain everything the view needs.
Instead what you could do is send back a model for your issue list page(s) which contains a property containing the issues list:
public class IssueListModel {
public List<Issue> Issues { get; set; }
}
Populate it in your issue list action:
public ActionResult IssueList(string projectName) // projectName may be null
{
var issueListModel = new IssueListModel();
issueListModel.Issues = SomeRepository.GetIssues(projectName); // whatever you need to send...
return View(issueListModel);
}
Then on your list pages you could loop through it:
#foreach (var issue in Model.Issues) {
<div>#issue.IssueName</div>
}
Or you could send the Issues collection down to a partial view:
#Html.RenderPartial("IssueList", Model.Issues)
You can type your partial view to expect List as the model:
#model List<MyProject.Models.Issue>
... and then loop through it in the partial view, this time doing a foreach on the model itself:
#foreach (var issue in Model) {
<div>#issue.IssueName</div>
}
Then what you can do is make a separate model for your project detail view which also contains a property containing Issues:
public class ProjectDetailModel {
public Project Project { get; set; }
public List<Issue> Issues { get; set; }
public string Whatever { get; set; }
}
In the controller you can populate this List using the same function that you would populate in your lists controller:
public ActionResult ProjectDetail(string projectName)
{
var projectDetailModel = new ProjectDetailModel();
projectDetailModel.Issues = SomeRepository.GetIssues(projectName, 10); // whatever you need to send
return View(projectDetailModel);
}
Then you can re-use the same exact partial view on your ProjectDetail view:
#Html.RenderPartial("IssueList", Model.Issues)
A long answer but I hope this is what you were looking for!
If you want to re-use presentation logic only, you can use partial view. If you want to re-use also controller's logic, you have to use child action combined with partial view.
Create a controller
public class IssuesController : Controller
{
[ChildActionOnly]
public PartialViewResult List(string projectName, int issueCount = 0)
{
IEnumerable<Issue> issueList = new List<Issue>();
// Here load appropriate issues into issueList
return PartialView(issueList);
}
}
Do not forget also to create appropriate partial view named List within the folder Issues.
Finally use this line within your project view
#{ Html.RenderAction("List", "Issues", new { projectName = "Px", issueCount = 10 } ); }
and this line within your issue list view
#{ Html.RenderAction("List", "Issues", new { projectName = "Px" } ); }
In your controller method return the view as named rather than just View()
ie...
public ViewResult IssueView1()
{ return View("Issue");}
public ViewResult IssueView2()
{return View("Issue");}
Let's say I have a theoretical MVC framework that uses a ViewData object to pass data from the controller to the view. In my controller, let's say I have some code like this (in pseudocode):
function GetClientInfo()
{
// grab a bunch of data from the database
var client = Database.GetClient();
var clientOrders = Database.GetClientOrders();
var clientWishList = Database.GetClientWishList();
// set a bunch of variables in the ViewData object
ViewData.set("client", client);
ViewData.set("clientOrders", clientOrders);
ViewData.set("clientWishList", clientWishList);
showView("ClientHomePage");
}
And then in my ClientHomePage view, I display the data like so:
<p>Welcome back, [ViewData.get("client").FirstName]!</p>
<p>Your order history:</p>
<ul>
[Html.ToList(ViewData.get("clientOrders")]
</ul>
<p>Your wishlist:</p>
<ul>
[Html.ToList(ViewData.get("clientWishList")]
</ul>
This is what I understand MVC to be like (please correct me if I'm wrong). The issue I'm having here is those magic strings in the view. How does the view know what objects it can pull out of the ViewData object unless it has knowledge of what the controller is putting in there in the first place? What if someone does a refactor on one of the magic strings in the controller, but forgets to change it in the view, and gets a runtime bug instead of a compile-time error? This seems like a pretty big violation of separation of concerns to me.
This is where I'm thinking that a ViewModel might come in handy:
class ClientInfo
{
Client client;
List clientOrders;
List clientWishList;
}
Then the controller creates an instance of ClientInfo and passes it to the view. The ViewModel becomes the binding contract between the controller and the view, and the view does not need to know what the controller is doing, as long as it assumes that the controller is populating the ViewModel properly. At first, I thought this was MVVM, but reading more about it, it seems like what I have in mind is more MVC-VM, since in MVVM, the controller does not exist.
My question is, what am I not understanding here about MVC vs. MVVM? Is referring to variables in the ViewData by magic strings really not that bad of an idea? And how does one insure that changes made in the controller won't adversely affect the view?
Your understanding of MVC is wrong, it stands for Model View Controller but you are missing the Model in your example. This is the typed entity that gets passed back to the View to do the rendering. In ASP.Net MVC you would use typed Views that also type the Model within the View so it is checked at compile time. This eliminates the need for magic strings (second part of your question).
In MVVM you have Model View ViewModel. This is a way of binding a ViewModel directly to the UI layer via a View which is used a lot in WPF. It replaces the need for a controller and it's generally a 1-to-1 mapping with the UI. It's just an alternative mechanism that solves the same problem (of abstraction and seperation of concerns) but better suited to the technology.
Theres some useful info here which might help understand the difference.
Best approach to use strongly typed views
Models:
public class ContentPage
{
public string Title { get; set; }
public string Description { get; set; }
}
public class ContentPagesModel
{
public ContentPage GetAboutPage()
{
var page = new ContentPage();
page.Title = "About us";
page.Description = "This page introduces us";
return page;
}
}
Controller:
public ActionResult About()
{
var model = new ContentPagesModel();
var page = model.GetAboutPage();
return View(page);
}
View:
#model Experiments.AspNetMvc3NewFeatures.Razor.Models.ContentPage
#{
View.Title = Model.Title;
}
<h2>About</h2>
<p>
#Model.Description
</p>
for more detail check out here
I case of using string as keys of ViewData - yes, it will be a lot of exceptions if someone will refactor code.
It sounds like your understanding of MVC is very old, probably based on MVC 1. Things have changed tremendously in the last couple of years.
Now we have strongly typed view models, and we have the ability to use expressions in the view, which by default aren't compile-time validated, but they can be for debug purposes (though it slows down the build a great deal).
What's more, we don't pass model data through ViewDate anymore (well, not directly anyways.. it's still passed that way but the framework hides it).
So in order accomplish what I asked in this post I did the following:
[iPhone]
[ActionName("Index")]
public ActionResult IndexIPhone()
{
return new Test.Areas.Mobile.Controllers.HomeController().Index();
}
[ActionName("Index")]
public ActionResult Index()
{
return View();
}
Which still serves the same view as the Index action method in this controller. Even though I can see it executing the Test.Areas.Mobile.Controllers.HomeController().Index() action method just fine. What's going on here? And how do I serve the Index view from Mobile area without changing the request URL (as asked in the original post referenced above)?
You have a few options:
Redirect to the Action you'd like to return: return RedirectToAction("Action-I-Want").
Return the View by name: return View("The-View-I-Want").
Note that with the 2nd approach you'd have to put your view in the "Shared" folder for all controllers to be able to find it and return it. This can get messy if you end up putting all your views there.
As a side note: The reason your work doesn't find the view is because default view engine looks for the view in the folder that "belongs" to the current executing controller context, regardless of what code you're calling.
Edit:
It is possible to group all "mobile" views in the same folder. On your Global.asax (or where ever you're setting up your ViewEngine, just add the path to your mobile View in the AreaViewLocationFormats. Mind you, you'll still have to name your views differently.
You can also write your own view engine. I'd do something like detecting the browser and then serving the right file. You could setup a convention like View.aspx, and View.m.aspx.
Anyhow, just take a look at WebFormViewEngine and you'll figure out what works best for you.
The easiest way to send a request to a view handled by another controller is RedirectToAction("View-Name", "Controller-Name").
There are overloads of View() that take route information that might work as well, but they'd require more effort to set up.
Well actually the easiest way is to make one version of your site programmed on standards instead of browser detection :D -- however in direct response to accomplish what it in a more of a ASP.NET mvc fashion, using:
RedirectToAction("ViewName", "ControllerName");
is a good method however I have found it is more practical if you feel you must program for different browser standards to create a primary view and an alternate "mobile" view under your controllers views. Then instead of writing special code on every controller, instead extend the controller like so.
public class ControllerExtended : Controller
{
private bool IsMobile = false;
private void DetectMobileDevices(){ .... }
}
Then modify your controller classes to instead say ControllerExtended classes and just add the one line to the top of each Action that you have alternate views of like so:
public class ApplicationsController : ControllerExtended
{
// GET: /Applications/Index
public ActionResult Index() {
this.DetectMobileDevices();
if(this.IsMobile){
return RedirectToAction("MobileIndex");
} else {
// actual action code goes here
return View();
}
}
}
Alternately you can use return View("ViewName"); but from my experience you want to actually perform different actions as opposed to just showing the result in a different view as in the case of presenting an HTML table as opposed to a Flex table to help iPhone users since there is no flash support in the iPhone, etc. (as of this writing)
For instance I have a model X with properties Title(string) and Valid(bool). I need to show same model on two separate pages with different field labels and input controls.
E.g. "Title" for title and "Valid" for valid on one form while "Destination" for title and "Returning" for valid on the other.
I guess the easiest way would be to have two different views for the same model. But is it really a MVC way to go?
Thanks
Well, let's say you have some View-folder called List, and one called Details - and displaying the Model in the two should be different.
You can create a DisplayTemplates folder within each of the two folders, and create a PartialControl with the same name as your Model, and also strongly type it to your Model.
In your different views you can then do <%= Html.DisplayFor( your model) %> or you can also use the regular <% Html.RenderParital("NameOfPartial", ModelX); %>
Edit
To try and approach the original question, maybe this could help you in some way (I posted this as an answer to a different question How to change [DisplayName“xxx”] in Controller?)
public class MyDisplayName : DisplayNameAttribute
{
public int DbId { get; set; }
public MyDisplayName(int DbId)
{
this.DbId = DbId;
}
public override string DisplayName
{
get
{
// Do some db-lookup to retrieve the name
return "Some string from DBLookup";
}
}
}
public class TestModel
{
[MyDisplayName(2)]
public string MyTextField { get; set; }
}
Maybe you could rewrite the custom-attribute to do some sort of logic-based Name-selection, and that way use the same PartialView for both model-variations?
Yes, two different Views is appropriate, as you are providing two different VIEWS of your MODEL.
However, are you sure you aren't shoehorning your data into a single model, when in fact it represents a different entity in each case?
Is it really the same model?
If they're two different entities with similar properties then I would create two separate view models. Any commonality could be put in an abstract base class or interface.
If it's the same model but just a different input screen then sure, reuse the model.
I would imagine the first case is probably the one that is relevant here.