I want to have a "Back" button that returns a user back to a page with a grid/table on it, but I'm having some trouble. The point of the back button is to allow my user's grid settings to be persisted between viewing records. The link that the Back button points to depends on what URL the user comes from. The url is set in the controller method. When the Url looks like the following...
http://localhost:49933/OpenOrder
... there is very little problem getting the back button to work. However, when the URL looks like this...
http://localhost:49933/OpenOrder?grid-sort=&grid-page=1&grid-pageSize=1000&grid-group=&grid-filter=CustomerName~contains~'TEST'
... sometimes the "Back" button won't work at all. I'm using a Telerik Kendo MVC grid, and this is what happens to the URL when a user filters the page.
I have noticed that the following code in the view works consistently on all links when there is no form tag (e.g. #Html.BeginForm())...
View
<a href='#Url.Content(#ViewBag.PreviousReferrer)' style="display:none;">
<input type="button" value="Back" />
</a>
But this doesn't work well with Views that do have the form tag. All I get is a Back button that I can click, but does absolutely nothing. It doesn't even show up in Fiddler.
Other things that I've tried include...
<input type="button" value="Back" onclick="#("window.location.href='" + #Url.Content(ViewBag.PreviousReferrer) + "'");" />
<input type="button" value="Back2" onclick="window.location.href = '#Url.Content(#ViewBag.PreviousReferrer)'" />
<input type="button" value="Back3" onclick="javascript:window.location=('#Url.Content(#ViewBag.PreviousReferrer)')" />
As stated previously, the ViewBag.PreviousReferrer property is set in the controller. Depending on whether a user is getting or posting, it will use either the Request.PreviousReferrer, or a session variable that I set. I won't include all my controller code since the rest of the pages work fine, but here's what it looks like when I set the ViewBag.PreviousReferrer in the gets and posts. At least one GET will come before any posts, so the session variable will always be set to something.
Controller
GET
string urlReferrer = Request.UrlReferrer == null ? string.Empty : Request.UrlReferrer.AbsoluteUri.ToUpper();
if (!string.IsNullOrEmpty(urlReferrer) && urlReferrer.Contains("OPENORDER"))
{
ViewBag.PreviousReferrer = Request.UrlReferrer.AbsoluteUri;
Session["OpenOrderPreviousReferrer"] = Request.UrlReferrer.AbsoluteUri;
}
else
{
ViewBag.PreviousReferrer = "~/OpenOrder";
}
POST
ViewBag.PreviousReferrer = Session["OpenOrderPreviousReferrer"] ?? "~/OpenOrder";
Ideally, this is what I want: One style of markup to go to the link in the PreviousReferrer REGARDLESS of whether or not the button is inside or outside of a form tag.
I think the problem might have something to do with HTML escape sequences, but I don't know if that's the technical term for it.
I had to create button links, that did a browser back "a lot" in one project, so created this HtmlHelper extension:
public static class ActionLinkButtonHelper
{
/// <summary>
/// Add a back button that generates a Javascript browser-back
/// </summary>
/// <param name="htmlHelper">HtmlHelper we are extending</param>
/// <param name="buttonText">Text to display on back button - defaults to "Back"</param>
/// <param name="actionName">Name of action to execute on click</param>
/// <param name="controller">Name of optional controller to send action to</param>
/// <returns></returns>
public static MvcHtmlString BackButton(this HtmlHelper htmlHelper, string buttonText="Back", string actionName="index", string controller=null, object routeValuesObject = null)
{
// Note: "Index is provided as a default
return ActionLinkButton(htmlHelper, buttonText, actionName, controller, routeValuesObject, new { onclick = "history.go(-1);return false;" });
}
}
and use it in the page like this:
#Html.BackButton()
or this:
#Html.BackButton("Button text", "action", "controller", new {id=routevalues})
It generates a button with a small snippet of JS that causes the browser to go back, so an actual link is not required unless JavaScript is disabled.
Supporting method (ActionLink as a button):
public static MvcHtmlString ActionLinkButton(this HtmlHelper htmlHelper, string buttonText, string actionName, string controllerName, object routeValuesObject = null, object htmlAttributes = null)
{
// For testing - create links instead of buttons
//return System.Web.Mvc.Html.LinkExtensions.ActionLink(htmlHelper, buttonText, actionName, controllerName, routeValues, htmlAttributes);
if (string.IsNullOrEmpty(controllerName))
{
controllerName = HttpContext.Current.Request.RequestContext.RouteData.Values["controller"].ToString();
}
RouteValueDictionary routeValuesDictionary = new RouteValueDictionary(routeValuesObject);
RouteValueDictionary htmlAttr = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
TagBuilder tb = new TagBuilder("input");
tb.MergeAttributes(htmlAttr, false);
string href = UrlHelper.GenerateUrl("default", actionName, controllerName, routeValuesDictionary, RouteTable.Routes, htmlHelper.ViewContext.RequestContext, false);
tb.MergeAttribute("type", "button");
tb.MergeAttribute("value", buttonText);
if (!tb.Attributes.ContainsKey("onclick"))
{
tb.MergeAttribute("onclick", "location.href=\'" + href + "\';return false;");
}
return new MvcHtmlString(tb.ToString(TagRenderMode.Normal).Replace("'", "\'").Replace(" "," "));
}
Views/Web.config change:
To make the above available to Intellisense/auto-complete in Views you need to add a new entry in the Views/web.config file <namespaces> like this:
<namespaces>
... [SNIP] ...
<add namespace="My.Lib.Name" />
</namespaces>
Where My.Lib.Name is the namespace where you have placed the ActionLinkButtonHelper shown above.
Related
I have a View displaying a list of items. I want to display an existing item when user select one to "edit", or display a new item when user clicks "create". This "single item" window will be called in many different places so I want combine the function "Edit" and "Create" into one view, which is not too bad. I understand I can do this by creating a view page on the same level of the caller, and when user clicks "submit" on this item window, the item controller do its job and then redirect back to the caller page (item list or other caller). But I want to try another approach, which is make this Item window as a pop up window, when user clicks on the "show modal" button, the caller page makes an ajax call to controller, passing item's data (or its id) as parameter, and the controller generate the MvcHtmlString dynamically, return to the caller, and caller display the popup window using the MvcHtmlString received. Everything seems to working, but the massive html code in my controller looks pretty messy.
So I'm wonder how to use HtmlHelper to generate MvcHtmlString in controller, for example, I have a object type "item", how can I do things like Html.LabelFor(item => item.Name), or Html.EditorFor(item => item.Name) in controller and get the MvcHtmlString?
You can extend your Htmlhelpers. If you want to just achieve edit/create button and you can change your viewmodel for condition or maybe you just want to use same model. You can use like this extension that i'm currently using on my views.
public static MvcHtmlString CustomActionLink(this HtmlHelper html, string action, string controller,
string displayText, bool isCreate)
{
if (isCreate)
{
var targetUrl = UrlHelper.GenerateUrl("Default", action, controller,
null, RouteTable.Routes, html.ViewContext.RequestContext, false);
var anchorBuilder = new TagBuilder("a");
anchorBuilder.MergeAttribute("href", targetUrl);
string classes = "btn btn-progress";
anchorBuilder.MergeAttribute("class", classes);
//Return as MVC string
anchorBuilder.InnerHtml = displayText;
return new MvcHtmlString(anchorBuilder.ToString(TagRenderMode.Normal));
}
else
{
var spanBuilder = new TagBuilder("span");
spanBuilder.MergeAttribute("class", "btn btn-progress");
spanBuilder.InnerHtml = displayText;
return new MvcHtmlString(spanBuilder.ToString(TagRenderMode.Normal));
}
}
I want to send a message to userID=3 by going to /MyController/Message/3
This executes Message() [get] action, I enter some text in the text area and click on Save to post the form
Message() [post] action saves the changes, resets the value of SomeText to empty string and returns to the view.
At this point I expect the text area to be empty because I have set ViewData["SomeText"] to string.Empty.
Why is text area value not updated to empty string after post action?
Here are the actions:
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Message(int ID)
{
ViewData["ID"] = ID;
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Message(int ID, string SomeText)
{
// save Text to database
SaveToDB(ID, SomeText);
// set the value of SomeText to empty and return to view
ViewData["SomeText"] = string.Empty;
return View();
}
And the corresponding view:
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<% using (Html.BeginForm())
{ %>
<%= Html.Hidden("ID", ViewData["ID"])%>
<label for="SomeText">SomeText:</label>
<%= Html.TextArea("SomeText", ViewData["SomeText"]) %>
<input type="submit" value="Save" />
<% } %>
</asp:Content>
The problem is that your ModelState is re-filled with the posted values.
What you can do is clear it on the Action that has the Post attribute :
ModelState.Clear();
The problem is the HtmlHelper is retrieving the ModelState value, which is filled with the posted data. Rather than hacking round this by resetting the ModelState, why not redirect back to the [get] action. The [post] action could also set a temporary status message like this:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Message(int ID, string SomeText)
{
// save Text to database
SaveToDB(ID, SomeText);
TempData["message"] = "Message sent";
return RedirectToAction("Message");
}
This seems to me like more correct behaviour.
The html helpers read the value from the ModelState. And there's no elegant way to override this behaviour.
But if you add this line after SaveToDB(ID, SomeText), it should work :
ModelState["SomeText"].Value =
new ValueProviderResult("", "", CultureInfo.CurrentCulture);
I tried everything, but only worked when I did something like this:
ModelState.Clear();
//This will clear the address that was submited
viewModel.Address = new Address();
viewModel.Message = "Dados salvos com sucesso!";
return View("Addresses", ReturnViewModel(viewModel));
Hope this helps.
Instead of using ModelState.Clear() which clears the whole modelstate, you can do ModelState.Remove("SomeText"), if you want to. Or render the Input without the htmlhelper-extensions.
They are designed to take the Value from ModelState instead of the Model (or viewdata).
That is a clientside behavior. I would recommend using javascript. If you use JQuery, you can do it like this:
<script type="text/javascript">
$(function(){ $("#SomeText").val("");});
</script>
I don't use Javascript anymore, but I believe in regular JS that it is like:
document.getElementById("SomeText").value = "";
(You would do this on one of the load events.
<body onload="...">
Hope this helps.
I am fairly certain the textarea is grabbing the value from the Request.Form under the hood since ViewData["SomeText"] is empty.
Is it possible that the model state has been updated with an error? I believe that it will pull the attempted value from the model state rather than from view data or the model if the model state isn't valid.
EDIT:
I'm including the relevant section of the source code from the TextArea HtmlHelper extension below. It appears to me that it does exactly what I expected -- if there has been a model error, it pulls the value from the model state, otherwise it uses it from ViewData. Note that in your Post method the "SomeText" key shouldn't even exist until you set it, i.e., it won't be carried forward from the version of the code that responds to the GET.
Since you explicitly supply a value to the ViewData, useViewData should be false, attemptedValue should be false unless an error has been set in the model state.
// If there are any errors for a named field, we add the css attribute.
ModelState modelState;
if (htmlHelper.ViewData.ModelState.TryGetValue(name, out modelState)) {
if (modelState.Errors.Count > 0) {
tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
}
}
// The first newline is always trimmed when a TextArea is rendered, so we add an extra one
// in case the value being rendered is something like "\r\nHello".
// The attempted value receives precedence over the explicitly supplied value parameter.
string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
tagBuilder.SetInnerText(Environment.NewLine + (attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : value)));
return tagBuilder.ToString(TagRenderMode.Normal);
Do s.th. like this:
add:
ModelState.Clear();
before the return statement of the submit buttons action method. Works for me. It could work for you.
I have a model that I am using in my view that is full of data. This data is then edited in the view. I need to figure out a way to resubmit this data back over to the controller.
Here is what I have so far.
VIEW:
#using (Html.BeginForm("DownloadCSV", "Respondents", FormMethod.Post))
{
#Html.HiddenFor(m => m.FilterSet)
<div class="btn btn-default pull-right" id="dispoCSV" onclick="$('#csvFormSubmit').click()">
<i class="icon-file-alt"></i> Disposition Report
</div>
<input id="csvFormSubmit" type="submit" style="display:none;" />
}
CONTROLLER:
[HttpPost]
public ActionResult DownloadCSV(RespondentsFilterSet model)
{
string csv = "Charlie, Test";
return File(new System.Text.UTF8Encoding().GetBytes(csv), "text/csv", "DispositionReport.csv");
}
MODEL:
public class RespondentsFilterSet : ColdListFilterSet
{
public List<int> OwningRecruiters { get; set; }
public List<int> RecruitingGroups { get; set; }
public override bool HasAtLeastOneFilter()
{
return base.HasAtLeastOneFilter() || OwningRecruiters.IsNotNullOrEmpty() || RecruitingGroups.IsNotNullOrEmpty();
}
public override ExpressionBase ToExpression()
{
var expr = base.ToExpression();
var expressions = expr == null ? new List<ExpressionBase>() : new List<ExpressionBase> { expr };
if (OwningRecruiters.IsNotNullOrEmpty())
{
expressions.Add(new InExpression<int> { Field = Create.Name<Respondent>(r => r.RecruitedBy), Values = OwningRecruiters });
}
if (RecruitingGroups.IsNotNullOrEmpty())
{
expressions.Add(new InExpression<int> { Field = Create.Name<Respondent>(r => r.RecruitingGroupId), Values = RecruitingGroups });
}
return expressions.Count == 0 ? null : BuildAndExpressionFromList(expressions);
}
}
I realize that my controller is not not finalized. I just have displaying some static csv. But I can't figure out why my model from my view is always null when returned to the controller.
Just look at your form. There's not a single input element (except the submit button). You cannot expect to get anything back on the server in this case.
Please read about HTML and how forms work in HTML. In HTML forms you have input fields. Things like text fields, hidden fields, checkboxes, radio buttons, ... - fields that the user interacts with get submitted to the server.
The fact that you have made your HttpPost controller action take some model as parameter doesn't mean at all that this parameter will be initialized. In ASP.NET MVC you have a default model binder. This model binder looks at what gets sent to the server as values when the form is submitted and uses the names of the fields to bind to the corresponding properties. Without input fields in the form, nothing gets sent to the server. Just use the debugging tools built into your web browser to inspect what exactly gets sent to the server.
Contrary to classic ASP.NET WebForms, ASP.NET MVC is stateless. There's no ViewState to remember your model.
So all this rambling is to say that you should read more about HTML forms first and understand the stateless nature of the web before getting into ASP.NET MVC. As far as your particular problem is concerned, well, assuming the user is not supposed to modify any values of the view model in your view throughout some input fields, you could simply include a hidden field containing the id of your model in the form. This id will then be sent to your POST controller action as parameter and you could use it to retrieve your original model from wherever it is stored (I guess a database or something).
I know how to create a url by using html.actionlink in the aspx file. But if I want to create the same url in a code behind file how would I do that?
The code behind idea for views in MVC was removed coz it didn't really seem to fit the MVC paradigm. Maybe you should consider creating your own Html Helpers instead. Doing this, extending existing actions like Html.ActionLink() is easy (and heaps of fun).
This example shows how i created a helper to tweak my login/logout links. Some ppl might argue whether this is a good use for a helper but it works for me:
/// <summary>
/// For the global MasterPage's footer
/// </summary>
/// <returns></returns>
public static string FooterEditLink(this HtmlHelper helper,
System.Security.Principal.IIdentity user, string loginText, string logoutText)
{
if (user.IsAuthenticated)
return System.Web.Mvc.Html.LinkExtensions.ActionLink(helper, logoutText, "Logout", "Account",
new { returnurl = helper.ViewContext.HttpContext.Request.Url.AbsolutePath }, null);
else
return System.Web.Mvc.Html.LinkExtensions.ActionLink(helper, loginText, "Login", "Account",
new { returnurl = helper.ViewContext.HttpContext.Request.Url.AbsolutePath }, null);
}
..and this is how i use it in the view (partial view to be exact):
<% =Html.FooterEditLink(HttpContext.Current.User.Identity, "Edit", "Logout (" + HttpContext.Current.User.Identity.Name + ")")%>
Take a look at this post by Scott Mitchell
http://scottonwriting.net/sowblog/posts/14011.aspx
(Since you say 'html.actionlink' which is an instance of the UrlHelper class I am assuming you are in a context where you don't have access to an instance of the UrlHelper class)
I want to send a message to userID=3 by going to /MyController/Message/3
This executes Message() [get] action, I enter some text in the text area and click on Save to post the form
Message() [post] action saves the changes, resets the value of SomeText to empty string and returns to the view.
At this point I expect the text area to be empty because I have set ViewData["SomeText"] to string.Empty.
Why is text area value not updated to empty string after post action?
Here are the actions:
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Message(int ID)
{
ViewData["ID"] = ID;
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Message(int ID, string SomeText)
{
// save Text to database
SaveToDB(ID, SomeText);
// set the value of SomeText to empty and return to view
ViewData["SomeText"] = string.Empty;
return View();
}
And the corresponding view:
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<% using (Html.BeginForm())
{ %>
<%= Html.Hidden("ID", ViewData["ID"])%>
<label for="SomeText">SomeText:</label>
<%= Html.TextArea("SomeText", ViewData["SomeText"]) %>
<input type="submit" value="Save" />
<% } %>
</asp:Content>
The problem is that your ModelState is re-filled with the posted values.
What you can do is clear it on the Action that has the Post attribute :
ModelState.Clear();
The problem is the HtmlHelper is retrieving the ModelState value, which is filled with the posted data. Rather than hacking round this by resetting the ModelState, why not redirect back to the [get] action. The [post] action could also set a temporary status message like this:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Message(int ID, string SomeText)
{
// save Text to database
SaveToDB(ID, SomeText);
TempData["message"] = "Message sent";
return RedirectToAction("Message");
}
This seems to me like more correct behaviour.
The html helpers read the value from the ModelState. And there's no elegant way to override this behaviour.
But if you add this line after SaveToDB(ID, SomeText), it should work :
ModelState["SomeText"].Value =
new ValueProviderResult("", "", CultureInfo.CurrentCulture);
I tried everything, but only worked when I did something like this:
ModelState.Clear();
//This will clear the address that was submited
viewModel.Address = new Address();
viewModel.Message = "Dados salvos com sucesso!";
return View("Addresses", ReturnViewModel(viewModel));
Hope this helps.
Instead of using ModelState.Clear() which clears the whole modelstate, you can do ModelState.Remove("SomeText"), if you want to. Or render the Input without the htmlhelper-extensions.
They are designed to take the Value from ModelState instead of the Model (or viewdata).
That is a clientside behavior. I would recommend using javascript. If you use JQuery, you can do it like this:
<script type="text/javascript">
$(function(){ $("#SomeText").val("");});
</script>
I don't use Javascript anymore, but I believe in regular JS that it is like:
document.getElementById("SomeText").value = "";
(You would do this on one of the load events.
<body onload="...">
Hope this helps.
I am fairly certain the textarea is grabbing the value from the Request.Form under the hood since ViewData["SomeText"] is empty.
Is it possible that the model state has been updated with an error? I believe that it will pull the attempted value from the model state rather than from view data or the model if the model state isn't valid.
EDIT:
I'm including the relevant section of the source code from the TextArea HtmlHelper extension below. It appears to me that it does exactly what I expected -- if there has been a model error, it pulls the value from the model state, otherwise it uses it from ViewData. Note that in your Post method the "SomeText" key shouldn't even exist until you set it, i.e., it won't be carried forward from the version of the code that responds to the GET.
Since you explicitly supply a value to the ViewData, useViewData should be false, attemptedValue should be false unless an error has been set in the model state.
// If there are any errors for a named field, we add the css attribute.
ModelState modelState;
if (htmlHelper.ViewData.ModelState.TryGetValue(name, out modelState)) {
if (modelState.Errors.Count > 0) {
tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
}
}
// The first newline is always trimmed when a TextArea is rendered, so we add an extra one
// in case the value being rendered is something like "\r\nHello".
// The attempted value receives precedence over the explicitly supplied value parameter.
string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
tagBuilder.SetInnerText(Environment.NewLine + (attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : value)));
return tagBuilder.ToString(TagRenderMode.Normal);
Do s.th. like this:
add:
ModelState.Clear();
before the return statement of the submit buttons action method. Works for me. It could work for you.