I have a problem with a web page that is rendered using the ASP.NET MVC (5.2.3) razor engine but which also uses AngularJS (1.7.4). The problem is that what should be rendered as plain text is being evaluated by AngularJS.
So, for example, in our model we have a property Name that when the value is, for example, {{1 + 1}} is being displayed as 2 i.e. the AngularJS expression is being evaluated:
The (redacted) cshtml page is along the following lines:
#model Models.SomeViewModel
<div ng-controller="RuleController" ng-form="ruleForm" novalidate>
<!-- stuff here -->
<div class="panel">
<div class="panel-heading">
<!-- stuff here -->
</div>
<div class="panel-body ng-cloak">
<!-- stuff here -->
<div class="row form-group">
#Html.LabelFor(model => model.Name, new HtmlAttributeBuilder().WithCssClass("col-sm-1 control-label"))
<div class="col-xs-12 col-sm-10 col-md-11 pull-right">
#Html.TextBoxFor(model => model.Name, new HtmlAttributeBuilder().WithCssClass("form-control form-group-margin").WithNgModel("viewModel.name").WithNgChange("updateName(viewModel.name)").WithRequired())
<span id="name-error" class="text-danger error-text" ng-model="errors.viewModel.Name">{{errors.viewModel.Name}}</span>
</div>
</div>
The C# class that is referenced in the Razor view - HtmlBuilder - inherits from `Dictionary'
[Serializable]
public class HtmlAttributeBuilder : Dictionary<string, object>
{
public HtmlAttributeBuilder WithCssClass(string cssClassName)
{
this.Add("class", cssClassName);
return this;
}
public HtmlAttributeBuilder WithNgModel(string property)
{
this.Add("ng-model", property);
return this;
}
public HtmlAttributeBuilder WithNgChange(string val)
{
this.Add("ng-change", val);
return this;
}
}
Interestingly, when I look at the value of $scope.viewModel.name in the RuleController I can see that it is {{1 + 1}} and not 2.
I have tried using ng-non-bindable and while it prevents evaluation of the Angular expression it also prevents persisted updates to the value as it removes two-way binding.
Have you tried this directive?
https://docs.angularjs.org/api/ng/directive/ngNonBindable
Note, this should probably go in the comments, but I don't have enough reputation.
Related
I want to call a specific Action with a parameter that corresponds to the the selected element in a tag.
The Model of the View can be simplified as follows:
public class Model
{
public string SelectedElement {get; set}
public List<SelectListItem> Elements { get; set; }
}
That i'm trying to display in the View:
<h2>Info</h2>
<div class="row">
<div class="form-group row">
<label for="selector-info" class="col-sm-2 col-form-label">Elements</label>
<div class="col-sm-10">
<select id="selector-info" asp-for="#Model.SelectedElement " asp-items="#Model.Elements">
</select>
</div>
</div>
</div>
<div class="row">
<a class="btn btn-primary"
asp-controller="Info"
asp-action="Export"
asp-route-element="#Model.SelectedElement ">
Exportar
</a>
</div>
And the action in the controller:
[Route("export/{element}")]
public async Task<IActionResult> Export(string element)
{
// Do something with the data
return RedirectToAction("Info");
}
Here what I'm trying to do is to call this specific Action that accepts a parameter, and fill it with the selcted element in the tag
But the generated HTML href was incorrect, and debugging I found out that the value Model.SelectedElement was not beeing set correctly.
On the other hand if I wrap this code inside a form and change the link to a button it works. My problem with this is that I'll have multiple buttons (at least 2) that will do different actions but in all cases the selected value of the tag must be passed when the button is clicked.
Is there a way to achieve this? I know I can get the value through JavaScript but I'm trying to find a way to do this directly in ASP without JS.
I have a parameterless Index for the HttpGet which works. But when I post it the HttpPost version of Index is invoked and the viewmodel object is passed in, but there is only the value of the dropdown in it. The rest is null (products, title)
[HttpPost]
public ActionResult Index(ProductsViewModel pvm)
{
// breakpoint on line 36, shows that pvm.Title is null and Products too.
return View(pvm);
}
My compilable and running example can be downloaded from my OneDrive http://1drv.ms/1zSsMkr
My view:
#model KleinKloteProductOverzicht.Models.ProductsViewModel
#using (Html.BeginForm("Index", "Products"))
{
<h2>#Html.DisplayFor(m => m.Title)</h2>
<input type="submit" value="post dit" /><br/>
<div class="row">
<div class="col-lg-2 col-md-2">
#Html.DropDownListFor(x => x.CurrentSort, EnumHelper.GetSelectList(typeof(SortOptions)), new { #class = "multiselect"})
</div>
</div>
if (Model.Products.Count() > 0)
{
<div class="row">
#foreach (var item in Model.Products)
{
#Html.DisplayFor(i => item.Name);
}
</div>
}
}
If I have this view model:
public class ViewModel
{
public string Name {get;set;}
public string SelectedLocation {get;set;}
public IEnumerable<SelectListItem> Locations {get;set;}
}
And your actions look like this:
public ActionResult MyForm()
{
var vm = new ViewModel
{
Locations = context.Locations.ToList() // Some database call
}
return View(vm);
}
[HttpPost]
public ActionResult MyForm(ViewModel vm)
{
vm.Locations // this is null
}
It is null because the model binder can't find a form control that is setting its data.
The <form> must set some data in the view for the model binder to pick it up.
<form>
Name: <input type="text" id="name" />
</form>
This will set the Name property on the view model, because the model bind can see the id of the form control and uses that to know what to bind to.
So in terms of your view, you need to make sure you wrap any content that you want to post back to the server with #using(Html.BeginForm())
Anyway this is my guess.
Well, you seem to be confused as to how [HttpPost] and form tags interact with eachother.
You see, when .NET MVC binds your parameters in your controller actions, it tries to derive that data from the request. For [HttpGet] it does this by looking at the query string.
For [HttpPost] calls, it also looks at the Request.Form. This variable is populated with the values of all input fields that were inside the form you submitted.
Now, this is your view:
#using (Html.BeginForm("Index", "Products"))
{
<h2>#Html.DisplayFor(m => m.Title)</h2>
<input type="submit" value="post dit" /><br/>
<div class="row">
<div class="col-lg-2 col-md-2">
#Html.DropDownListFor(x => x.CurrentSort, EnumHelper.GetSelectList(typeof(SortOptions)), new { #class = "multiselect" })
</div>
</div>
if (Model.Products.Count() > 0)
{
<div class="row">
#foreach (var item in Model.Products)
{
#Html.DisplayFor(i => item.Name);
}
</div>
}
}
You only have one select tag (generated by Dropdownlistfor) but no other inputs. That's why .NET MVC cannot infer any other data for your view model.
If you change your view to this:
#model KleinKloteProductOverzicht.Models.ProductsViewModel
#using (Html.BeginForm("Index", "Products"))
{
<h2>#Html.DisplayFor(m => m.Title)</h2>
<input type="submit" value="post dit" /><br/>
<div class="row">
<div class="col-lg-2 col-md-2">
#Html.DropDownListFor(x => x.CurrentSort, EnumHelper.GetSelectList(typeof(SortOptions)), new { #class = "multiselect" })
</div>
</div>
if (Model.Products.Count() > 0)
{
<div class="row">
#for (var i = 0; i < Model.Products.Count; i++)
{
#Html.DisplayFor(model => model.Products[i].Name)
#Html.HiddenFor(model => model.Products[i].ID)
}
</div>
}
}
You'll see I've added a hidden input (<input type="hidden">) for the product id. Note that the product name still will be null.
I would suggest you follow a tutorial on .NET MVC and read up on some of the concepts behind it, because the very fact that you ask this question reveals that you have much to learn.
Best of luck!
P.S. One last tip: #Html.Blablabla writes directly to your view. You usually don't need that ";" at the end, because it will be inside your generated html.
Your property is not associated with a "postable" control, therefore it will not be submitted along with the form data. If your really want to get the value in your Title property, just set it as a hidden input.
#Html.HiddenFor(m => m.Title)
A label will not be posted when submitting a form but an input will. This is exactly what HiddenFor does; it creates a hidden input element which will be picked up by the form submit.
I'm writing an MVC app which ends up accessing a SQL database. On my edit page, I previously had every item available to be edited that is in the model. Recently I was asked to no longer allow the user to edit the primary keys. I did a quick change to change the primary key fields (in this example, there are 2 of them) from an EditorFor to a DisplayFor. The new view is this:
#model App.Data.Item
#{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Item</h4>
<hr />
#Html.ValidationSummary(true)
<div class="form-group">
<strong>ID:</strong>
<div class="col-md-10">
<p>#Html.DisplayFor(model => model.ID)</p>
</div>
</div>
<div class="form-group">
<strong>ID2:</strong>
<div class="col-md-10">
<p>#Html.DisplayFor(model => model.ID2)</p>
</div>
</div>
<div class="form-group">
<strong>Description:</strong>
<div class="col-md-10">
<p>#Html.EditorFor(model => model.Description)
#Html.ValidationMessageFor(model => model.Description)</p>
</div>
</div>
<br />
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<button type="submit" class="btn btn-primary">Submit <i class="fa fa-caret-right"></i></button>
</div>
</div>
</div>
}
It used to work with the full editing. Now the data is displayed properly, as expected. However, when submit is pressed, Null values are passed back to the controller for the values that are displayed.
These are the edit functions in my controller. ItemService.Edit() just saves the data to the server. It works correctly.
[Authorize]
public ActionResult Edit(string id)
{
if (id == null)
{
//return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
string[] vals = id.Split('|');
ItemAttribute itemAttribute = itemAttributeService.Find(int.Parse(vals[0]), vals[1]);
if (itemAttribute == null)
{
return HttpNotFound();
}
return View(itemAttribute);
}
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize]
public ActionResult Edit([Bind(Include = "ID,ID2,Description")]
Item item)
{
if (item.Description == null)
{
ModelState.AddModelError("Description", "Description cannot be null.");
}
if (ModelState.IsValid)
{
itemService.Edit(item);
return RedirectToAction("../Home/Index/");
}
return View(item);
}
Lastly, my data model:
public class Item
{
public int ID { get; set; }
public string ID2 { get; set; }
public string Description { get; set; }
}
Why is the data no longer being passed back to the second function, and how do I get it to pass correctly so that I can save it to the database?
You need to have an input element generated for the items that you want returned. Currently, you are only displaying two of your model elements and have no associated input with them. As a result, they will not be POSTed back to the server.
To get them to post to the server and not "editable" from a textbox, add a Html.HiddenFor() helper for each of the items that you need returned.
<div class="form-group">
<strong>ID:</strong>
<div class="col-md-10">
<p>#Html.DisplayFor(model => model.ID)</p>
<p>#Html.HiddenFor(model => model.ID)</p>
</div>
</div>
<div class="form-group">
<strong>ID2:</strong>
<div class="col-md-10">
<p>#Html.DisplayFor(model => model.ID2)</p>
<p>#Html.HiddenFor(model => model.ID)</p>
</div>
</div>
However, keep in mind that anyone can edit the HTML using Firebug or Chrome console tools and submit any value that they want for any field, even if you did not want to change it. Be sure that when you are persisting the changes to the database, you are NOT including these fields as part of the update.
Try this, just before this line of code:
#Html.DisplayFor(model => model.ID)
put in this for debugging:
#Html.LabelFor(model => model.ID)
Tell us what you see...
If you see the label then check your controller, in particular the parameter it takes on the post. It should take and Item of type ITEM per your model.
Before the controller receives the data MVC has to try to populate the data... It converts name/value pairs to model types with values secretly. If you don't see any data after you are in the controller it's usually because the names were not found!
Before some people start yelling, I realize there is similar questions up, but their all dealing with single entries...
I have a long form and want to use a generic method of dealing with the re-displaying of data.
Please note : this is an asp.Net MVC 4 application using razor2 views
Example of one of the fields,
<div class="control-group">
<label class="control-label">#Html.LabelFor(m => m.Name)
<span class="required">*</span>
</label>
<div class="controls">
#Html.TextBoxFor(m => m.Name, new { #name = "nameInput" })
</div>
</div>
My think was to add an ID to each Textbox...
Example of how is might be displayed on the confirmation view..
<div class="control-group">
<label class="control-label">Name:</label>
<div class="controls">
<span class="text display-value" data-display="nameInput"></span>
</div>
</div>
Then render that value using data-display with the ID...
jQuery that I thought would deal with it...
var displayConfirm = function() {
$('.display-value', form).each(function(){
var input = $('[name="'+$(this).attr("data-display")+'"]', form);
if (input.is(":text") || input.is("textarea")) {
$(this).html(input.val());
} else if (input.is("select")) {
$(this).html(input.find('option:selected').text());
}
});
}
Unfortunately this does not appear to be working correctly....
Can anyone point out / re-solve the issue ?
Got it, I needed to use the name in the model rather then assigning names.
Got correct names from "page view source", and simply plugged that value into data-display tag.
I have below view model
public class QuestionarrieAnswersViewModel
{
public long QuestionID { get; set; }
public string Question { get; set; }
[Required(ErrorMessage="required")]
[StringLength(255, ErrorMessage = "Maximum 255 characters are allowed.")]
public string Answer { get; set; }
}
and i am generating view in below way
#model List<BusinessLayer.Models.ViewModel.QuestionarrieAnswersViewModel>
#using (Ajax.BeginForm("SaveQuestionarrie", "Member", FormMethod.Post, new AjaxOptions { OnBegin = "OnBegin", OnComplete = "OnComplete" }, new { #class = "form-horizontal" }))
{
for(int i=0;i<Model.Count;i++)
{
<div class="control-group">
<div class="head_form">
<label class="control-label">#Model[i].Question</label>
<div class="controls">
#Html.TextAreaFor(m=>m[i].Answer)
#Html.ValidationMessageFor(m => m[i].Answer)
#Html.HiddenFor(m=>m[i].QuestionID)
</div>
</div>
</div>
}
<div class="control-group">
<div class="controls">
<button class="btn" type="submit">Save</button>
</div>
</div>
}
I have set dataannotation on Answer field in above model but its not applying in above view while it works if i generate view in below way
#model BusinessLayer.Models.ViewModel.QuestionarrieAnswersViewModel
#using (Ajax.BeginForm("SaveQuestionarrie", "Member", FormMethod.Post, new AjaxOptions { OnBegin = "OnBegin", OnComplete = "OnComplete" }, new { #class = "form-horizontal" }))
{
#Html.TextAreaFor(m => m.Answer)
#Html.TextAreaFor(m => m.QuestionID)
<div class="control-group">
<div class="controls">
<button class="btn" type="submit">Save</button>
</div>
</div>
}
What's going wrong here...
In order to fire those validation rules, you'll need to use an EditorFor instead of a TextAreaFor.
It's because there's an outstanding issue with validation of TextArea's, see here: http://aspnet.codeplex.com/workitem/8576.
This is due to a bug in the version of jquery.validate.unobtrusive.js that was released with ASP.NET MVC3. This answer is on the same bug, the solution to this is to upgrade to the latest version of jquery.validate.unobtrusive.js - either grab it from an MVC4 project or update using NuGet.
The jquery.validate.unobtrusive.js script doesn't seem to have a version number so if you search in the script for a function called escapeAttributeValue, then this is a version of the script that has this bug fix.
The problem that is addressed in the bug fix is how to handle markup generated having name attributes containing characters that need escaping in a jQuery selector. In this case
<textarea cols="20" name="[0].Answer" rows="2"></textarea>
needs this selector
$('[name=\\[0\\]\\.Answer]')
The client-side DataAnnotation (validation) does not work for the Html.TextAreaFor() helper.
To make it work, you have to decorate the 'Answer' property with the [DataType(DataType.MultilineText)] attribute. And in the view, use Html.EditorFor() helper instead of the Html.TextAreaFor() helper mehthod.
See similar SO answer asp.net mvc TextAreaFor is not getting validated as a required field.