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");}
Related
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).
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.
Still Learning MVC3, EF. For now I am connecting to MySql but I believe that will not be relevant. For simplicity, I decided to use one database for my test application and I have included a category to differentiate the data. For eg I have a news, events,info and pages categories. Now when it comes to listing contents in views for example at the homepage, I want to list latest 5 news items(news category), latest 5 events(events category), welcome text(info category). i have been able to create partialViews to list these at the different sections of the homepage. But I feel am doing this wrongly since in each of these PartialViews I am querying the same table over and over and just filtering with where cat=....in the LINQ query.
Can you please confirm if that should be the practice or there is a better way to do this.
You could do the following:
Controller:
public ActionResult Home()
{
IEnumerable<MyDateRecords> myData = LinqQueryToGetAllTheDataUnFiltered();
ViewData.Model = new MyViewData { MyData = myData; }
return View();
}
ViewModel class:
public class MyViewData
{
List<MyDataRecords> MyData { get; set; }
List<MyDataRecords> News { get { return MyData.Where(m => m.Category = "News"); } }
List<MyDataRecords> Events { get { return MyData.Where(m => m.Category = "Events"); } }
}
View:
#model MyViewModel
#Html.Partial("NewsPartial", Model.News)
#Html.Partial("EventsPartial", Model.Events)
Partial:
#model IEnumerable<MyDataRecord>
This way we only queried for the data once and just passed a different set to each partial
For an uncomplicated way of presenting this type of data, what you are doing is fine. You should look at the OutputCacheAttribute for any PartialView Method you use on your Controller.
This is pretty inefficient. But it's good that you noticed this because querying that database is often the bottleneck in any given request.
For starters you should retrieve that data into a dictionary or model and then pass it to the partial views to render similar to what Bassam outlined. Ideally, this should be taken care of in the Controller to stick to the MVC paradigm and then passed to the main view, which would then pass the appropriate data to the partial views.
Once you get more advanced/familiar with ASP.NET MVC, you can start looking into caching. I'd stay away from caching for right now since it get somewhat tricky if you have data that is rapidly changing since you need to start worrying about updating/synchronizing/etc.
I have a tag cloud that I'm including on nearly all of the views that I have in my site. I realize that I can put it on a master page. The bigger question is, how do I pass it the data from the database? I'd like to avoid having to fetch the data in every single controller and action. Is there an easy way to do this?
Use #{ Html.RenderAction("TagCloud", "SomeController"); }.
public class SomeController : Controller {
public ActionResult TagCloud() {
var model = // fetch data for tagcloud
return View("~/Views/Shared/Tagcloud.ascx", model);
}
}
It's quite common to load a view that has it's own view model, f.e. because it's shared among a lot of different pages and scenario's, in it's own action.
I think I'm missing some fundamentals on how MVC forms work. I have a search form on my home page that has five or six different fields a user can search on. So I have this POSTing to my results action just fine. The Result action looks like this:
[HttpPost]
public ActionResult Results(SearchModel model)
{
ResultsModel results = new ResultsModel();
results.ResultList = SearchManager.Search(model).ToList();
return View("Results", results);
}
I've simplified the above method for this post, but the idea is the same. So this all works fine. My results page shows up with the list of results and my user is at the following URL:
http://www.site.com/results
So...now I want to do something fairly common. I have two dropdown lists on the results page. "Sort by" and "# of results per page". How do I do that and send the full set of model data back to the controller so I can query with the new parameters? In reality, the SearchModel class has about 60 different fields. Potentially all of that data could be contained in the model. How do you persist that to a page "post back"?
This same question has me a little stumped about how to do paging as well. My paging links would go to a URL like:
http://www.site.com/results/2
But that assumes that we're responding to a GET request (I don't want 60 fields of data in the querystring) and that the model data is passed between GET requests, which I know isn't the case.
As I said, I think I'm missing some fundamentals about working with MVC 3, models and form posts.
Can anyone help point me in the right direction here? I'll be happy to edit/update this post as needed to clarify things.
EDIT: I also wanted to point out, I'd like to avoid storing the view model in a Session variable. This site will eventually end up being load balanced in a web farm and I'm really trying to avoid using Session if possible. However, if it's the only alternative, I'll configure another session state provider, but I'd prefer not to.
You can add your current SearchModel parameters to the route values for your form. Several versions of BeginForm allow you to pass in an object/RouteValuesDictionary.
#Html.BeginForm("Action", "Controller", new { SearchModel = Model }, FormMethod.Post)
This should pass-through your current SearchModel values so you can re-use them to get the next page. You need to have a controller action defined that will accept any current-page form values as well as the SearchModel.
I have not done this with form posts, but from what I have done and from what the docs say, this is where I would start. Of course, this also means that each of your page number "links" on the page will need to be doing posts. That is really inconvenient for users if they want to be able to use the Back button in the browser.
In this context, you can try to define a route that allows the page number to appear as a part of the URL -- "Action/Controller/{page}". However, I am not sure how that will work given that the form is doing a post.
Response to Comment:
Yeah, you can use Route Values to add the SearchModel to each page link, but as I said in the comment above, since the links will do a "get," your users will see the SearchModel serialized as a part of the link.
Either way, using Route Values is your answer to getting back your original SearchModel without using hidden fields, Session, or TempData.
Your SearchModel class needs to contain your search criteria and your results. Something like below. If you use a PagedList for your results then it will contain the current page, total pages, total items, etc. You can limit the amount of information in your page by only writing the search criteria that contain values.
public class SearchModel
{
public string Product { get; set; }
public string Sku { get; set; }
public string Size { get; set; }
public string Manufacturer { get; set; }
// etc...
public PagedList ResultsList { get; set; }
}
[HttpPost]
public ActionResult Results(SearchModel model)
{
model.ResultList = SearchManager.Search(model).ToList();
return View(model);
}
One of the options I'm coming up with here is to implement a distributed caching system that supports acting as a custom session provider (i.e. Memcached or Windows Server AppFabric), thereby allowing me to use TempData (and Session) in a load balanced environment like so:
[HttpPost]
public ActionResult Results(SearchModel model)
{
ResultsModel results = new ResultsModel();
results.ResultList = SearchManager.Search(model).ToList();
TempData["SearchModel"] = model;
return View("Results", results);
}
[HttpGet]
public ActionResult Results(int? page)
{
SearchModel model = (SearchModel)TempData["SearchModel"];
ResultsModel results = new ResultsModel();
results.ResultList = SearchManager.Search(model).ToList();
TempData["SearchModel"] = model;
return View("Results", results);
}
Any thoughts on this approach? Seems like a lot to have to go through just to get search parameters passed between requests. Or maybe I was just spoiled with this all happening behind the scenes with WebForms. :)
This seems to be another interesting option for Webforms spoiled guy ;) Persisting model state in ASP.NET MVC using Serialize HTMLHelper
Some kind of ViewState incarnation. It is part of MVC Futures . Not sure how long it is in Futures project and why it cannot get into main lib.