ASP Core Razor Pages Change BindProperty Value on Post - c#

I'm creating a form using one razor page (I know this may not be a good idea, but I'm trying to do some [not so] rapid prototyping. Basically, the first step is that the user enters a bunch of data, and the next step is that the user will upload some files. My page model looks like this:
public class CreateModel : PageModel
{
private readonly DefaultDbContext _context;
public CreateModel(DefaultDbContext context)
{
_context = context;
}
public async Task<IActionResult> OnGet(Guid id)
{
FileUpload = false;
return Page();
}
[BindProperty]
public bool FileUpload { get; set; } // Stored in a hidden field in my cshtml
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://aka.ms/RazorPagesCRUD.
public async Task<IActionResult> OnPostAsync()
{
FileUpload = true; // Hidden field is always false
return Page();
}
}
This is what my form looks like:
<form method="post" enctype="multipart/form-data">
<input type="hidden" asp-for="FileUpload" />
#if (!Model.FileUpload)
{
// Do some stuff
}
else
{
<div class="form-group">
<label asp-for="Upload" class="control-label"></label>
<input type="file" asp-for="Upload" multiple />
<span asp-validation-for="Upload" class="text-danger"></span>
</div>
}
<div class="form-group">
#if (Model.FileUpload)
{
<input type="submit" value="Finished" class="btn btn-primary" />
}
else
{
<input type="submit" value="Next" class="btn btn-primary" />
}
</div>
</form>
When I click submit the first time, I would expect FileUpload to be true, which it is when I step through the .cshtml page in the debugger. The problem is that when the page is sent back to the browser, the value is always false:
<input type="hidden" data-val="true" data-val-required="The FileUpload field is required." id="FileUpload" name="FileUpload" value="False" />
What am I doing wrong?

A couple of things should be addressed:
<input type="hidden"> elements are useful only when you are trying to send information (contained in those elements) FROM the page TO the model, not the vice versa.
[BindProperty] attribute is used when you are trying to map some information FROM the page TO the model's property, not the vice versa:
Model Binding in Razor Pages is the process that takes values from HTTP requests and maps them to [...] PageModel properties. Model Binding
In the line #if (Model.FileUpload), you are not extracting any information from the page's hidden input element, but from the model itself. Your code should work as is (if you ignore the hidden input element).
That being said, you could completely remove the hidden input (and the [BindProperty] attribute, since nothing could be bound to FileUpload after you remove the input) and you should be good to go.
If you do, however, wish to change the hidden input value accordingly, you could do that in a couple of ways:
Remove [BindProperty] from the FileUpload and keep everything else the same.
Keep the [BindProperty], but render the changed FileUpload value:
<input type="hidden" asp-for="FileUpload" value="#Model.FileUpload"/>

Related

Preserve values when submitting Razor Pages form

I'm using Razor Pages 7. My page has filtering, sorting and paging.
MyPage.cshtml.cs
public SelectList Products { get; set; }
public int Sort { get; set; }
public int Page { get; set; }
public int Filter { get; set; }
public async Task OnGetAsync(int sort, int page, int filter) {
//...
}
MyPage.cshtml
<form method="get" asp-page="" asp-route-sort=#Model.Sort asp-route-page=#Model.Page>
<select name="filter" asp-items=#Model.Products>
<option selected value="0">Filter by product</option>
</select>
<input type="submit" value="submit" />
</form>
That correctly renders as:
<form method="get" action="/mypage?sort=10&page=3">
...
</form>
But when I submit the form, the get action only binds the filter argument. The sort and page arguments are not bound from the form's action.
Does RazorPages offer a way to bind those too?
(I could do it with JavaScript, but I'm hoping there's a RazorPages-native approach.)
UPDATE
I found a way, using hidden inputs. In the form:
<input type="hidden" name="sort" value=#Model.Sort />
<input type="hidden" name="page" value=0 /> <!-- reset page on filter change -->
But this is a manual approach with many magic strings. Is there a RazorPages "native" way?
That correctly renders as:
<form method="get" action="/mypage?sort=10&page=3"> ... </form>
It already complied asp-route-somekey=someval to html correctly,appended the key value pairs to the query part.
But when I submit the form, the get action only binds the filter
argument. The sort and page arguments are not bound from the form's
action
In fact, it's an issue related with Html5,when you submit form with GET method,query parameters of action attribute would be dropped.If you check the url,it would be "/mypage?filter=someval"
The only solution to appending query parameters you've self-answered
If you would accept add the arguements to the route part
regist the route of your page like:
#page "/MyPage/{key1?}/{key2?}"
The result:
I just tried with hard coding to reproduced the error:
#{
var dic = new Dictionary<string, string>()
{
{"key1","10"},
{"key2","2"}
};
}
<form method="get" asp-page="/MyPage" asp-all-route-data=#dic >
<select name="filter" >
<option selected value="0">Filter by product</option>
<option value="1">item1</option>
<option value="2">item2</option>
</select>
<input type="submit" value="submit" />
</form>

why post method doesn't refresh the page in the action method?

I know the post action method below is anti-pattern, but still I assume to see a new page with Name being set to null. But when I click the submit button, the page is not reloaded and I still see the old name displayed, is this a browser thing or asp.net core framework thing?
public class HomeController : Controller
{
private IRepository repository;
public HomeController(IRepository repo)
{
repository = repo;
}
// ...
public IActionResult Create() // create a Employer that has a name in the browser
{
return View();
}
[HttpPost]
public IActionResult Create(Employee model)
{
model.Name = ""; // <----------------set it to null so I expect to see the Name being empty in the reponse
return View(model);
}
}
// view file, Create.cshtml:
#model Employee
#{
ViewData["Title"] = "Create Employee";
}
<h2>Create Employee</h2>
<form asp-action="Create" method="post">
<div class="form-group">
<label asp-for="Id"></label>
<input asp-for="Id" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Name"></label>
<input asp-for="Name" class="form-control" />
</div>
<div class="form-group">
<label asp-for="DOB"></label>
<input asp-for="DOB" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Role"></label>
<select asp-for="Role" class="form-control" asp-items="#new SelectList(Enum.GetNames(typeof(Role)))"></select>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
Firstly, a get method to invoke the Create() action method and fill some data:
After clicking submit button, the page still shows the original name where I exepct to see a empty name since I modify it in the post action method
so why the name still exists after the second request?
So why the name still exists after the second request?
Well, altough, you have rebind your model property value. However, your current model state still remain unchanged. Therefore, you are getting the older value even after the form resubmission. To overcome this issue, please try following code snippet:
Solution:
You have clear your current model state by using ModelState.Clear(); method before assigning the new value.
So, your create method should be as bellow:
[HttpPost]
public IActionResult Create(Employee model)
{
ModelState.Clear();
model.Name = "";
return View(model);
}
Note: Rest of the code will remain unchanged.
Output:
ModelState.Clear(); wwill resolve your issue completely.

How to prevent model binding of disabled or readonly input fileds in Razor view HTML in ASP.NET Core?

So I have complex object that should be displayed on the page. Some properties of this object should be disabled (or readonly) for users in certain access roles. However, I noticed that if I edit this <input disabled .../> in devtools and remove disabled property and add arbitrary text, then model binder will happily take this value and put it in form model when submitted via OnPost action.
Here is example code:
<form method="post">
<div class="form-group col-sm-6">
<label asp-for="FormModel.FirstName" class="control-label">First Name</label>
<input asp-for="FormModel.FirstName"
disabled="disabled"
class="form-control"
/>
</div>
<div class="form-group col-sm-6">
<label asp-for="FormModel.LastName" class="control-label">Last Name</label>
<input asp-for="FormModel.LastName"
class="form-control"
/>
</div>
<input type="submit" value="Save" class="btn btn-primary" />
</form>
And the page model:
public class IndexModel : PageModel
{
private readonly MyDbMock _myDb;
[BindProperty]
public FormModel FormModel { get; set; }
public IndexModel(MyDbMock myDb)
{
_myDb = myDb;
}
public void OnGet()
{
FormModel = new FormModel
{
FirstName = _myDb.FormModel.FirstName,
LastName = _myDb.FormModel.LastName
};
}
public IActionResult OnPost()
{
_myDb.FormModel = FormModel;
return RedirectToAction(nameof(OnGet));
}
}
public class FormModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class MyDbMock
{
public FormModel FormModel { get; set; } = new FormModel
{
FirstName = "John",
LastName = "Smith"
};
}
When I open this page for the first time I get John in the disabled input with First name label. This is fine. Then when I click Save my OnPost method will get FormModel object where FirstName is null. This is reflected in the updated page. Can this be prevented and tell model binder just to not touch this property? Ie. leave it as John when form is submitted and not set it to null because of it being disabled?
The other issue I have is that an advanced user can remove disabled attribute from the First Name input and just submit arbitrary value. Can this also be prevented by telling model binder same thing as above? Eg. leave First Name field as-is when it was sent in GET request, no updates.
Here is the demo of all this:
I know I can handle this in backend code by preventing updating of the disabled fields. However I am looking some more elegant and declarative way if it exists. In my main project the form model is much complex and has objects within objects so ideally I'd like to skip adding additional logic to action handler for this.
Try to add a hidden input to bind FormModel.FirstName,and change the name of the disabled input,so that the value of disabled input will not be binded to FormModel.FirstName.
<form method="post" >
<div class="form-group col-sm-6">
<label asp-for="FormModel.FirstName" class="control-label">First Name</label>
<input asp-for="FormModel.FirstName" name="FirstName"
disabled="disabled"
class="form-control" />
<input asp-for="FormModel.FirstName" hidden/>
</div>
<div class="form-group col-sm-6">
<label asp-for="FormModel.LastName" class="control-label">Last Name</label>
<input asp-for="FormModel.LastName"
class="form-control" />
</div>
<input type="submit" value="Save" class="btn btn-primary" />
</form>
result:

Input Tag Helper Not Working with Razor Code

I want to combine an input tag helper with razor code to set an attribute but I cannot get the two technologies to work together. I am simply trying to set the disabled attribute on the input field based on the value of view model property.
When i put the razor code after the asp-for tag the razor intellisense is not recognized and the field is not disabled as expected...
<input asp-for="OtherDrugs" #((Model.OtherDrugs == null) ? "disabled" : "") class="form-control" />
Rendered output...
<input type="text" id="OtherDrugs" name="OtherDrugs" value="" />
When i put the razor code before the asp-for tag the tag helper intellisense is not recognized and the field is not set with the view model properties as expected...
<input #((Model.OtherDrugs == null) ? "disabled" : "") asp-for="OtherDrug" class="form-control" />
Rendered output...
<input disabled asp-for="OtherDrugs" class="form-control" />
Note that combining tag helpers and razor does work if the razor code is inside a class attribute. Unfortunately input fields require the disabled attribute and not the disabled class for bootstrap 3.
Is there a way to make this work?
To render the disabled input element, you simply need to add a disabled attribute. All the below will render a disabled input text element.
<input type="checkbox" disabled />
<input type="checkbox" disabled="disabled" />
<input type="checkbox" disabled="false" />
<input type="checkbox" disabled="no" />
<input type="checkbox" disabled="enabled" />
<input type="checkbox" disabled="why is it still disabled" />
In Asp.NET Core, You can extend the existing input tag helper to create a readonly input tag helper.
Extend the InputTagHelper class, add a new property to identify whether the input should be disabled or not and based on this value, add the "disabled" attribute to the input.
[HtmlTargetElement("input", Attributes = ForAttributeName)]
public class MyCustomTextArea : InputTagHelper
{
private const string ForAttributeName = "asp-for";
[HtmlAttributeName("asp-is-disabled")]
public bool IsDisabled { set; get; }
public MyCustomTextArea(IHtmlGenerator generator) : base(generator)
{
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (IsDisabled)
{
var d = new TagHelperAttribute("disabled", "disabled");
output.Attributes.Add(d);
}
base.Process(context, output);
}
}
Now to use this custom textarea helper, you need to call the addTagHelper method in _ViewImports.cshtml.
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
#addTagHelper *, YourAssemblyNameHere
Now in your view, you can specify the asp-is-disabled attribute value.
<input type="text" asp-for="OtherDrugs"
asp-is-disabled="#Model.OtherDrugs==null"/>
You can use ASP Core tag helper like this:
<input asp-for="Name" />
and then put [Editable(false)] for your property like this:
[Editable(false)]
public string Name {set;get;}
then you should extend InputTagHelper:
[HtmlTargetElement("input", Attributes = ForAttributeName)]
public class ExtendedInputTagHelper : InputTagHelper
{
private const string ForAttributeName = "asp-for";
public ExtendedInputTagHelper(IHtmlGenerator generator)
: base(generator) { }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var isContentModified = output.IsContentModified;
if (For.Metadata.IsReadOnly)
{
var attribute = new TagHelperAttribute("disabled", "disabled");
output.Attributes.Add(attribute);
}
if (!isContentModified)
{
base.Process(context, output);
}
}
}
and finally import your TagHelper in _ViewImports.cshtml:
#addTagHelper *, <your assembly name>
the advantage of this solution is putting logic in Model and preserving MVC principles.
For me the TagHelper extension is an overkill, since I needed just two inputs disabled/enabled, based on some model values.
Therefore I went for the simpliest (probably not the best) approach - #if/else.
#if (Model.OtherDrugs == null)
{
<input asp-for="OtherDrug" disabled="disabled" class="form-control" />
}
else
{
<input asp-for="OtherDrug" class="form-control" />
}

Model not passed in ASP.NET MVC 3

I have an Index View, where I got a form containing a partial view for the different formulars.
#Html.ValidationSummary(true, "Beheben Sie die Fehler, und wiederholen Sie den Vorgang.")
#using (Html.BeginForm())
{
object mod = null;
switch (Model.Step)
{
case 1:
Html.RenderPartial("Step1", Model.Step1);
break;
case 2:
Html.RenderPartial("Step2", Model.Step2);
break;
default:
Html.RenderPartial("Step0");
break;
}
<p>
#if (Model.Step > 100000)
{
<button name="button" value="Zurück" />
}
#if (Model.Step != 0)
{
<input type="submit" name="submit" value="Zurück" /> <input type="submit" name="submit"
value="Weiter" id="Weiter" /> <input type="submit" name="submit" value="Abbrechen" />
}
</p>
}
In my Controller I got something like this:
[HttpPost]
public ActionResult Index(InputModel model, string submit, HttpPostedFileBase file)
{
if (String.IsNullOrEmpty(submit))
submit = "";
if (submit == "Weiter")
model.Step++;
if (submit == "Zurück")
model.Step--;
The InputModel has several "sub-models" like this:
public Step1Model Step1 { get; set; }
public Step2Model Step2 { get; set; }
public Step3Model Step3 { get; set; }
Which are passed to the partial view to fill them.
The problem now is that I always get an empty model in my HttpPost in my controller.
What am I doing wrong?
What am I doing wrong?
You are using partials. Partials don't respect the navigational context. So when you look at your generated HTML source you will see the following:
<input type="text" name="SomeProperty" value="some value" />
instead of the correct one which is expected by the default model binder:
<input type="text" name="Step1.SomeProperty" value="some value" />
So when you submit this form you are not properly binding to the Step1 property. Same remark obviously for the other complex properties.
One possibility is to use editor templates instead of partials because they preserve the navigational context and generate proper names for your input fields.
So instead of:
Html.RenderPartial("Step1", Model.Step1);
use:
#Html.EditorFor(x => x.Step1, "Step1")
and then move your ~/Views/SomeController/Step1.cshtml partial to ~/Views/SomeController/EditorTemlpates/Step1.cshtml.
If you don't want to use editor templates but keep with the partials you could change the temlpate prefix inside the partial. So for example inside Step1.cshtml partial you could put the following in the top:
#{
ViewData.TemplateInfo.HtmlFieldPrefix = "Step1";
}
Now when you inspect your generated HTML source proper names should be emitted for the input fields. Personally I would recommend you the editor templates approach though to avoid hardcoding the prefix and making this partial less reusable compared to the editor templates.

Categories

Resources