Why sometimes #ViewBag.Title contains the right page title while #Page.Title is null? We are debugging our View/Layout code and we noticed this difference.
Thanks.
When your are using asp.Net MVC you have a few tools you can use to get data to your page.
The model you send to the view.
The ViewBag.
TempData.
Cookies.
Session.
Each one of theese has their own use cases
The example we will be using is a basic
List and update View
For a collection of companies
The Model
The model Should be used whenever you have a defined dataset being sent to a view and should contain the primary data for the page and usally a model is specific to a page or controller.
The ViewBag
The ViewBag should be used whenever you need to send general data to a page that is not defined on the model or data that is used across your view tree.
Temp Data
Temp Data is as the name states storage for temporary data this should be used in cases where you are unsure if the data will reach its destination or when you want to pass data between actions without adding parameters.
Cookies
Cookies are like Global variables the host data across your entire application.
Session
Sessions like cookies are global variables but differ in their lifetime sessions can be used for things like Storing form data between requests dont use session to do this rather use something like http://garlicjs.org/
Lets look at the example
We have the following ViewModel
public class Company
{
public int? id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
The layout page looks like this
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
//This looks for the Title property on the ViewBag and inserts it in the title tags
<title>#ViewBag.Title</title>
</head>
<body>
#Html.Action("Header", "PageElements")
#RenderBody()
</body>
</html>
The Header Action on the PageElements Contoller looks like this
public PartialViewResult Header()
{
ViewBag.CurrentLink = HttpContext.Current.Request.Cookies["CurrentLink"];
return PartialView();
}
The Header Partial View looks like this
<header>
<ul>
<li class="#(ViewBag.CurrentLink == "Home" ? "Current" : "")">
#Html.ActionLink("Home", "Index", "Home")
</li>
<li class="#(ViewBag.CurrentLink == "Companies" ? "Current" : "")">
#Html.ActionLink("Companies", "Index", "Companies")
</li>
</ul>
</header>
The controller action for update looks like this
public ViewResult Update(int id)
{
//Gets a company from the database
var model = db.GetCompany(id);
//Sets The Title property on the ViewBag to Update : CompanyName
ViewBag.Title = String.Format("Update : {0}", model.Name);
//Sends the company to the view
return View(model);
}
The update view looks like this
#model Application.Models.Company
#{
Layout = "_layout.cshtml";
}
#using (Html.BeginForm())
{
#Html.HiddenFor(model => Model.id)
#Html.LabelFor(model => Model.Name)
#Html.TextBoxFor(model => Model.Name)
#Html.LabelFor(model => Model.Description)
#Html.TextAreaFor(model => Model.Description)
<button>Submit</button>
}
The Post Action for Update looks like this
[HttpPost]
public ActionResult Update(Company model)
{
//Attempts to update the model
if (model.Update())
{
//Set the message to update succeeded
TempData["Message"] = String.Format("{0} Successfully updated");
}
else
{
//Set the message to update failed
TempData["Message"] = String.Format("{0} filed to update");
}
return RedirectToAction("Index");
}
The Controller action for companies looks like this
public ViewResult Index()
{
//Index is the entrypoint for companies so we set the currentlink to companies while we are in companies
HttpContext.Current.Request.Cookies["CurrentLink"] = "Companies";
//Gets the success or failure message from the temp data
ViewBag.Message = TempData["Message"];
//Sets The Title property on the ViewBag to List of companies
ViewBag.Title = "List of companies";
var model = db.GetAllCompanies();
return View(model);
}
The view for the list looks like this
#model IEnumrable<Application.Models.Company>
#{
Layout = "_layout.cshtml";
}
<span>#ViewBag.Message</span>
<ul>
#foreach (var company in Model)
{
<li>#company.Name : #company.Description #Html.AnchorLink("Edit", "Update", new { company.id}) </li>
}
</ul>
Lets discuss how the flow of this application works
The Index Action gets triggered on companies.
We set the current link to companies in a Cookie
Then we set the Title in the ViewBag
We check the TempData for messages and put them into the ViewBag
Then we put the collection of companies into a Model and send it to the page.
Now razor kicks in and starts renders the page first
Then the layout renders and gets the Title value from the ViewBag
Then the header partial view renders and gets the currentlink value from a Cookie
We click the update button for a company
The update action runs
Gets the company from a database and sends it to the view in a Model
We change the title to update in the ViewBag
The page renders first and puts the company data on the form, from the Model
Then the layout renders and Gets the title value from the ViewBag
Then the header renders and gets the currentlink from a cookie (the value is still companies we did not have to change it)
Then we submit the form and add a message to the TempData
now we are back at the company list and we can get the success or failure message from the TempData
We used the Model to send Specific data to the view.
We used the ViewBag to send General data to the view and the layout.
We used TempData to pass data between actions without using parameters.
We used Cookies to Store data that wouldn't change for a while.
The correct is using ViewBag.Title, because it is on the context of your ViewBag property of your View while Page.Title is something that comes from asp.net webforms. That is the reason why Page.Title is null, there is no context for it.
Related
I have a .NET MVC project with Razor views and I would like to implement search functionality that consists of a dropdown list, a text box and a search button.
The problem is that I would like to implement this search snippet in my _Layout.cshtml file outside of the #RenderBody() call. This means that the search functionality would be accessible on every page (it would be located at the very top right corner).
I'm trying to find out what is a good way of implementing this. I can get it to work but it would involve adding same code (do get dropdown values) to all controllers and actions.
ViewBag.States = new SelectList(db.States, "Id", "Name");
Is there a better way to implement this? It feels very repetitive to do it this way.
You can have a child action method which returns the partial view needed for your header and call this action method in your layout.
Create a view model for the properties needed.
public class AllPageVm
{
public int SelectedItem { set; get; }
public List<SelectListItem> Items { set; get; }
}
Now create an action method in any of your controller. Mark this action method with ChildActionOnly decorator.
public class HomeController : Controller
{
[ChildActionOnly]
public ActionResult HeaderSearch()
{
var vm = new AllPageVm()
{
Items = db.States
.Select(a => new SelectListItem() {Value = a.Id.ToString(),
Text = a.Name})
.ToList()
};
return PartialView(vm);
}
Now in the HeaderSearch.cshtml partial view, you can render whatever markup you want for your search header. Here is a simple example to render the dropdown. You may update this part to include whatever markup you want (Ex : a form tag which has textbox, dropdown and the button etc)
#model AllPageVm
<div>
<label>Select one state</label>
#Html.DropDownListFor(a => a.SelectedItem, Model.Items, "Select")
</div>
Now in your layout, you can call this child action method
<div class="container body-content">
#Html.Action("HeaderSearch", "Home")
#RenderBody()
<hr/>
<footer>
<p>© #DateTime.Now.Year - My ASP.NET Application</p>
</footer>
</div>
Make sure you are calling PartialView method from the HeaderSearch child action method instead of View method. If you call View method, it will recursively call the same method and you will get a StackOverflow exception
My partialview:
#model Alina_2017.Models.DropDownModel
<h2>Groepen</h2>
<div>
<div>
#using (Html.BeginForm("SelectGroup", "~/Controllers/WerkvormController"))
{
#Html.DropDownListFor(x => x.selectedItem, new SelectList(ViewBag.groepen, "id", "Naam"), "Select", new { #class = "form-control" })
<input type="submit" id="zoekgroep" value="Zoeken" />
}
</div>
</div>
My main view:
#model Alina_2017.Models.WerkvormModel
#{
ViewBag.Title = "Index";
}
#Html.Partial("~/Views/DropDown/Groepen.cshtml")
//More irrelevant html
My controller:
public ActionResult Index()
{
ViewBag.groep1 = convertWerkvorm(db.Werkvormens.Where(f => f.GroepenWerkvormID == 1).ToList());
ViewBag.groep2 = convertWerkvorm(db.Werkvormens.Where(f => f.GroepenWerkvormID == 2).ToList());
ViewBag.groep3 = convertWerkvorm(db.Werkvormens.Where(f => f.GroepenWerkvormID == 3).ToList());
setViewBags();
return View();
}
[HttpPost]
public ActionResult SelectGroup(DropDownModel model)
{
// the value is received in the controller.
var selectedItem = model.selectedItem;
Debug.WriteLine(selectedItem);
return View("Index");
}
I'm getting a HTTP Error 404.0 - Not Found. Is it possible to call an action from a different controller? The reason it's in a partial view is because I'm using two different models + I'll be using the partialview in multiple other views (at least once I get it to work).
Your controller's name is wrong.
Replace
#using (Html.BeginForm("SelectGroup", "~/Controllers/WerkvormController"))
with
#using (Html.BeginForm("SelectGroup", "Werkvorm"))
You can verify the actual post URL if you view your source in browser, or check network tab in the browser's development tools.
The second argument to the BeginForm() method is simply the name of the controller, not its file:
#using (Html.BeginForm("SelectGroup", "Werkvorm"))
{
}
You can post to any server-side action from anywhere. There's no limit based on how the view is rendered because once everything is rendered it's all just client-side markup no matter where it came from.
As a learning exercise, examine the actual rendered markup in your browser's debugging tools and see the URLs created for the forms. Regardless of how the partial views are arranged, which controller returned the view, what the models are, etc... It's all just HTML in the end. You can even manually write a simple .html file with a form on it which successfully posts to a server-side ASP.NET MVC action.
I'm new to the whole MVC 3 style coding. I'm running into an issue, but first here's how I have my website laid out.
_Layout.cshtml contains
#if (Request.IsAuthenticated)
{
<div class="span2">
#Html.Partial("_NavigationPartial")
</div>
<div class="span10">
#RenderBody()
</div>
}
else
{
#RenderBody()
}
#RenderBody will display my Profile.cshtml file that contains the following:
#{
ViewBag.Title = "My Profile";
}
<div class="row-fluid well">
<div class="page-header">
<h1>
Profile</h1>
</div>
#{
Html.RenderPartial("ChangePersonalInformationPartial");
Html.RenderPartial("ChangePasswordPartial");
}
</div>
As you see, I have two Partials (One to change the Personal Information, the other to Change the Password).
Each one of these Partials uses it's own Model (ChangePersonalInformationModel and ChangePasswordModel).
My problem comes when I click submit on my ChangePasswordPartial, it reloads the _Layout.cshtml page but this time only loads up ChangePasswordPartial.cshtml. I need it to load up Profile.cshtml. But, if I go ahead and change under my AccountController.cs the return View(); to return View("Profile"); I get an error saying:
The model item passed into the dictionary is of type
'PROJECT.Models.ChangePasswordModel', but this dictionary requires a
model item of type 'PROJECT.Models.ChangePersonalInformationModel'.
How can I fix this problem?
Thanks!
Basically you have to make a redirect to the profile action in the ChangePassword action once you saved the password information.
UPDATE:
First you should have a common model say ProfileModel that wraps up the ChangePasswordModel and ChangePersonalInformationModel.
So here are the actions that displays the profile information for viewing and editing.
// this action will returns a views that displays profile info
public ViewResult Profile(string username)
{
ProfileModel model = .. get the profile from database based on username
return View(model);
}
// this action will returns the profile info for editing or adding a new profile
public ViewResult EditProfile(string username)
{
.. if the profile already exists get from database
ProfileModel model =
.. if this is a new profile create an empty model
ProfileModel model = new ProfileModel();
model.ChangePasswordModel = new ChangePasswordModel();
model.ChangePersonalInformationModel = new ChangePersonalInformationModel();
return View(model);
}
Your EditProfile.cshtml will be like this
#model Models.ProfileModel
...
#{
Html.RenderPartial("ChangePersonalInformationPartial",
Model.ChangePersonalInformationModel);
Html.RenderPartial("ChangePasswordPartial", Model.ChangePasswordModel);
}
...
This will be your ChangePassword action
[HttpPost]
public ActionResult ChangePassword(ChangePasswordModel model)
{
if(ModelState.IsValid)
{
// save the ChangePasswordModel to database and display the profile info
// or even you can redirect to EditProfile for more editing
return RedirectToAction("Profile");
}
.. there are validation errors so get the complete profile model from database
.. the ChangePasswordModel form will be filled by the details entered in the form
.. and not from the db details this will be taken care by the framework itself.
ProfileModel model =
return View("EditProfile", model);
}
I've looked over a bunch of other reports of this, but mine seems to be behaving a bit differently. I am returning PartialViewResults for my child actions, so that's not the source of the recursion. Here's a dumbed down version of what I have.
// The Controller
[ChildActionOnly]
public ActionResult _EditBillingInfo()
{
// Generate model
return PartialView(model);
}
[HttpPost]
public ActionResult _EditBillingInfo(EditBillingInfoViewModel model)
{
// Update billing informatoin
var profileModel = new EditProfileViewModel()
{
PartialToLoad = "_EditBillingInfo"
};
return View("EditProfile", profileModel);
}
[ChildActionOnly]
public ActionResult _EditUserInfo()
{
// Generate model
return PartialView(model);
}
[HttpPost]
public ActionResult _EditUserInfo(EditUserInfoViewModel model)
{
// Update user informatoin
var profileModel = new EditProfileViewModel()
{
PartialToLoad = "_EditUserInfo"
};
return View("EditProfile", profileModel);
}
public ActionResult EditProfile(EditProfileViewModel model)
{
if (String.IsNullOrEmpty(model.PartialToLoad))
{
model.PartialToLoad = "_EditUserInfo";
}
return View(model);
}
// EditProfile View
#model UPLEX.Web.ViewModels.EditProfileViewModel
#{
ViewBag.Title = "Edit Profile";
Layout = "~/Views/Shared/_LoggedInLayout.cshtml";
}
<div>
<h2>Edit Profile</h2>
<ul>
<li class="up one"><span>#Ajax.ActionLink("Account Information", "_EditUserInfo",
new AjaxOptions { UpdateTargetId = "EditProfileDiv", LoadingElementId = "LoadingImage" })</span></li>
<li class="up two"><span>#Ajax.ActionLink("Billing Information", "_EditBillingInfo",
new AjaxOptions { UpdateTargetId = "EditProfileDiv", LoadingElementId = "LoadingImage" })</span></li>
</ul>
<img alt="Loading Image" id="LoadingImage" style="display: none;" src="../../Content/Images/Misc/ajax-loader.gif" />
<div id="EditProfileDiv">
#Html.Action(Model.PartialToLoad)
</div>
</div>
The partial views are both forms for updating either the user information or billing information.
I debugged through this and found what is happening, but cannot figure out why. When a user browses to EditProfile, it load up with the _EditUserInfo partial and the form is there for editing. When you change some info and submit the form it hangs and you get a StackOverflowException in the EditProfile view on the call to #Html.Action(). What happens is on the initial visit to EditProfile, the #Html.Action calls the HttpGet version of _EditUserInfo. You make some changes to the user info and click submit. Once the information is updated the EditProfile view is returned again, but this time #Html.Action calls the HttpPost version of _EditUserInfo which updates the user information again, returns the EditProfile view again and the #Html.Action calls the HttpPost version of _EditUserInfo... You get where this is going. Why after form submission does it call the post version and not the get version like it did for the initial visit to EditProfile?
Thanks for any help!
I might be getting this a bit wrong, it's been a long day so, but in EditProfile you set PartialToLoad (if it's empty) to "_EditUserInfo", then in _EditUserInfo you set it again to _EditUserInfo, won't this create a loop that behaves as what you are experiencing?
I've read several different posts on paging w/ in MVC but none describe a scenario where I have something like a search form and then want to display the results of the search criteria (with paging) beneath the form once the user clicks submit.
My problem is that, the paging solution I'm using will create <a href="..."> links that will pass the desired page like so: http://mysite.com/search/2/ and while that's all fine and dandy, I don't have the results of the query being sent to the db in memory or anything so I need to query the DB again.
If the results are handled by the POST controller action for /Search and the first page of the data is rendered as such, how do I get the same results (based on the form criteria specified by the user) when the user clicks to move to page 2?
Some javascript voodoo? Leverage Session State? Make my GET controller action have the same variables expected by the search criteria (but optional), when the GET action is called, instantiate a FormCollection instance, populate it and pass it to the POST action method (there-by satisfying DRY)?
Can someone point me in the right direction for this scenario or provide examples that have been implemented in the past? Thanks!
My method is to have an Action that handles both the post and the get scenarios.
This is my which can be handled by both GET and POST methods:
public ViewResult Index([DefaultValue(1)] int page,
[DefaultValue(30)] int pageSize,
string search,
[DefaultValue(0)] int regionId,
[DefaultValue(0)] int eventTypeId,
DateTime? from,
DateTime? to)
{
var events = EventRepo.GetFilteredEvents(page, pageSize, search, regionId, eventTypeId, from, to);
var eventFilterForm = EventService.GetEventFilterForm(from, to);
var eventIndexModel = new EventIndexModel(events, eventFilterForm);
return View("Index", eventIndexModel);
}
The eventFilterForm is a presentation model that contains some IEnumerable<SelectListItem> properties for my search form.
The eventIndexModel is a presentation model that combines the eventFilterForm and the results of the search - events
The events is a special type of IPagedList. You can get more information and code for that here and here. The first link talks about IPagedList where as the second link has an Advanced Paging scenario which you should need.
The advanced paging has the following method that I use:
public static string Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, RouteValueDictionary valuesDictionary)
And I use it like so:
<%= Html.Pager(Model.Events.PageSize,
Model.Events.PageNumber,
Model.Events.TotalItemCount,
new
{
action = "index",
controller = "search",
search = ViewData.EvalWithModelState("Search"),
regionId = ViewData.EvalWithModelState("RegionId"),
eventTypeId = ViewData.EvalWithModelState("EventTypeId"),
from = ViewData.EvalDateWithModelState("From"),
to = ViewData.EvalDateWithModelState("To")
}) %>
This creates links that look like:
/event/search?regionId=4&eventTypeId=39&from=2009/09/01&to=2010/08/31&page=3
HTHs,
Charles
Ps. EvalWithModelState is below:
PPs. If you are going to put dates into get variables - I would recommend reading my blog post on it... :-)
/// <summary>
/// Will get the specified key from ViewData. It will first look in ModelState
/// and if it's not found in there, it'll call ViewData.Eval(string key)
/// </summary>
/// <param name="viewData">ViewDataDictionary object</param>
/// <param name="key">Key to search the dictionary</param>
/// <returns>Value in ModelState if it finds one or calls ViewData.Eval()</returns>
public static string EvalWithModelState(this ViewDataDictionary viewData, string key)
{
if (viewData.ModelState.ContainsKey(key))
return viewData.ModelState[key].Value.AttemptedValue;
return (viewData.Eval(key) != null) ? viewData.Eval(key).ToString() : string.Empty;
}
Make the Search parameter part of your View Model:
public SearchViewModel
{
string SearchParameters { get; set; }
List<SearchObjects> SearchResults { get;set; }
}
Then just set the Search Textbox equal to SearchParameters.
You cannot "store" the search query unless you bring back ALL results and then store those in the page somehow. That is horribly inefficient. The web is stateless, so you will have to go back to the database and re-query for more results.
I understand what you are saying; you could change the form to use buttons and post the page back everytime. Or, you could pass all the criteria in the URL for the paging as querystring variables. Or you could use JQuery to do the post (it has a $.post method that can be invoked from a link click or other click (http://api.jquery.com/jQuery.post/).
HTH.
This problem goes away if you include the search text, as well as the current results page, in your querystring instead of POSTing the search text. As an added benefit, your users can then bookmark their search results.
To do this your search button just needs to build the GET request URL using the current value of the search box. This can be done either in javascript or by using GET as your search form's method attribute, e.g. <form method="get" action="/search">.
I recommend cacheing your search results and giving them an ID. Then for each paging link, you can reference the search ID as a parameter (on each search page link) and in your action, pull it from cache, then query over it.
Using this method, you don't need to worry about anything other than the first POST submit of the search form.
Refer to my post for more details.
I had this same problem and here's what I did.
Download PagedList from Nuget
Change your form to do a GET and create a ViewModel type similiar to this (if you love AdventureWorks and Model Binding as much as I do):
`
using PagedList;
namespace SearchFormResultPagingExample.Models {
public class SearchViewModel {
public int? Page { get; set; }
public string EmailAddress { get; set; }
public string LastName { get; set; }
public IPagedList<Contact> SearchResults { get; set; }
public string SearchButton { get; set; }
}
}
`
3.Use the ViewModel as the parameter to your controller's action method
using System.Linq;
using System.Web.Mvc;
using SearchFormResultPagingExample.Models;
using PagedList; //NOTE: use Nuget to reference PagedList
namespace SearchFormResultPagingExample.Controllers {
public class SearchController : Controller {
const int RecordsPerPage = 25;
public ActionResult Index(SearchViewModel model) {
if (!string.IsNullOrEmpty(model.SearchButton) || model.Page.HasValue) {
var entities = new AdventureWorksEntities();
var results = entities.Contacts.Where(c => c.LastName.StartsWith(model.LastName) && c.EmailAddress.StartsWith(model.EmailAddress))
.OrderBy(o => o.LastName);
var pageIndex = model.Page ?? 0;
model.SearchResults = results.ToPagedList(pageIndex, 25);
}
return View(model);
}
}
}
Use the pager on in your View:
#model SearchFormResultPagingExample.Models.SearchViewModel
#using PagedList.Mvc;
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm("Index", "Search", FormMethod.Get)) {
#Html.ValidationSummary(false)
<fieldset>
<legend>Contact Search</legend>
<div class="editor-label">
#Html.LabelFor(model => model.EmailAddress)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.EmailAddress)
#Html.ValidationMessageFor(model => model.EmailAddress)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.LastName)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.LastName)
#Html.ValidationMessageFor(model => model.LastName)
</div>
<p>
<input name="SearchButton" type="submit" value="Search" />
</p>
</fieldset>
}
#if (Model.SearchResults != null && Model.SearchResults.Count > 0) {
foreach (var result in Model.SearchResults) {
<hr />
<table width="100%">
<tr>
<td valign="top" width="*">
<div style="font-weight: bold; font-size:large;">#result.LastName, #result.FirstName</div>
#result.Title<br />
#result.Phone<br />
#result.EmailAddress
</td>
</tr>
</table>
}
<hr />
#Html.PagedListPager(Model.SearchResults,
page => Url.Action("Index", new RouteValueDictionary() {
{ "Page", page },
{ "EmailAddress", Model.EmailAddress },
{ "LastName", Model.LastName }
}),
PagedListRenderOptions.PageNumbersOnly)
}
MVC will coerce the querystring to and from your ViewModel type parameter. It's very slick!