ASP.Net Core Razor Pages: How to return the complex model on post? - asp.net-core

I created a new ASP.Net Core 2 (Razor Pages) Project
My model is:
public class FormularioGenerico
{
public FormularioGenerico()
{
}
public string IP { get; set; }
public List<string> items { get; set; } = new List<string>();
}
On the page I put
on the page.cshtml.cs
public class EditarModel : PageModel
{
[BindProperty]
public FormularioGenerico ff { get; set; }
[BindProperty]
public string Message { get; set; }
public void OnGet()
{
this.ff = new FormularioGenerico();
ff.IP = "C# FORM";
ff.items.Add("OK1");
ff.items.Add("OK2");
ff.items.Add("OK3");
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var m = ModelState.IsValid; // true
Debug.WriteLine(this.ff.IP); // is Always returning null
Debug.WriteLine(this.ff.items.Count); // is Always returning null
}
}
on the page.cshtml:
#model Formulario.Pages.EditarModel
...
<h1>#Model.ff.IP</h1>
#foreach (var i in Model.ff.items)
{
<div>#i</div>
}
<button type="submit">Enviar</button>
The items are correctly output. But the complete object does not go to the OnPost.
The problem is: The model is not coming fully populated on the OnPost.
How to receive the full object that was created on the OnGet, plus the changes made by the user on the form, on the post to OnPostAsync() ?

The BindProperty attribute is used to inform ASP.NET Core that the values that the form submitted should be mapped to the specified object. In your case you set the values for the ff property but you do not have the equivalent input values so that ASP.NET Core will get these values in order to store them back to the ff property.
In order to make it work you will have to replace your razor code with the following code:
<form method="post">
<h1>#Model.ff.IP</h1>
<input asp-for="#Model.ff.IP" type="hidden" /> #* create a hidden input for the IP *#
#for (int i = 0; i < Model.ff.items.Count(); i++)
{
<input asp-for="#Model.ff.items[i]" type="hidden" /> #* create a hidden input for each item in your list *#
<div>#Model.ff.items[i]</div>
}
<button type="submit">Enviar</button>
</form>
Very important. To make this work you can not use the foreach loop because ASP.NET core will not be able to find the values. You will have to use a for loop.
The inputs that I added are hidden because I guess you do not want them to be visible but you can remore the type="hidden" so that you will be able to see them. Every change that you make to these inputs will be submitted to the OnPostAsync method.

Related

Blazor: Binding values to select/dropdown when using component tag helper in a Razor page

I'm building an app with Blazor Webassembly and I'm doing all the user management related bits of the system server side. I'm using the component tag helper to enable me to use Razor components on my server side Razor pages, but I'm struggling to get my multiselect dropdown list working.
I've created a custom component (Dropdown.razor) that makes use of Radzen's dropdown component which looks like this:
#typeparam TItem
<RadzenDropDown AllowClear="true" AllowFiltering="true" FilterCaseSensitivity="Radzen.FilterCaseSensitivity.CaseInsensitive"
Multiple="true" Placeholder="Please Select" #bind-Value="#BindValue"
Data="#Data" TextProperty="#TextProperty" ValueProperty="Id" />
#code {
[Parameter] public IEnumerable<int> BindValue { get; set; }
[Parameter] public IList<TItem> Data { get; set; }
[Parameter] public string TextProperty { get; set; }
}
and then I'm pulling that through into my Edit.cshtml Razor page like this:
<div class="form-group">
<label>Line Manager(s)</label>
<component type="typeof(Dropdown<UserDto>)" render-mode="ServerPrerendered" param-BindValue="#Model.Input.SelectedLineManagers"
param-Data="#Model.Input.LineManagers" param-TextProperty="FullName" />
</div>
This works in that it loads the component, pulls through a list of line managers and preselects any previously selected line managers (using data I've just input directly into the database). However, when I go to post the form, Input.SelectedLineManagers is always null - i.e. it doesn't appear to be binding properly when I update the dropdown.
What am I doing wrong? Using ASP.NET Core hosted Blazor Webassembly (.net 6).
Below is the code behind by Edit.cshtml file that gets called when the form is submitted.
public async Task<IActionResult> OnPostAsync(string userId)
{
//code omitted for brevity
var userManagerLinks = new List<UserManagerLink>();
if (Input.SelectedLineManagers != null)
{
foreach (var item in Input.SelectedLineManagers)
{
userManagerLinks.Add(new UserManagerLink
{
UserId = int.Parse(userId),
ManagerId = item
});
}
}
var userSub = HttpContext.User?.FindFirstValue("Sub");
await _unitOfWork.SaveUser(userSub);
return RedirectToPage("Index");
}

Razor pages view renders original form value instead of modified when reshown

Moving from ASP.NET Core MVC to Razor pages, there must be an issue in my understanding of passing data to a Razor view.
Here's a simple view:
#page
#model TestModel
#{
System.Diagnostics.Debug.WriteLine(Model.Name);
}
<form class="form-horizontal" method="get">
Name: <input type="text" class="form-control" asp-for="Name">
<button type="submit" class="btn btn-default">Send...</button>
</form>
And here is the view model class with one event handler:
public class TestModel : PageModel
{
[BindProperty(SupportsGet = true)]
public string Name { get; set; } = "";
public TestModel() {}
public IActionResult OnGet()
{
Name += "N";
return Page();
}
}
Then running the project:
Debug.WriteLine shows "N" as expected
input fields has "N" default value as expected
overwrite "N" in the input field eg. to "A" then press Send button
Debug.WriteLine shows "AN" as expected so view has got the value modified by OnGet()
the input field in the page itself contains the value "A" instead of "AN", the generated HTML contains:
value="A"
View does not render the modified Model.Name value but the original from the form data.
How can it be corrected to make view to render the modified string?
You can try to add ModelState.Clear(); in OnGet handler.When binding data in client side,it will get value from ModelState prior to Model.
public class TestModel : PageModel
{
[BindProperty(SupportsGet = true)]
public string Name { get; set; } = "";
public TestModel() {}
public IActionResult OnGet()
{
Name += "N";
ModelState.Clear();
return Page();
}
}

Razor Pages Custom Tag Helper 2 Way Bind

I am wanting to create a custom tag helper in razor pages which binds to a custom model but the value is not being read back into the modal on post, below is my TagHelper code
[HtmlTargetElement("kenai-date", TagStructure = TagStructure.WithoutEndTag)]
public class Date : TagHelper
{
//public string Value { get; set; }
public ModelExpression AspFor { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "input";
output.TagMode = TagMode.SelfClosing;
output.Attributes.Add("value", this.AspFor.Model);
}
}
I am using the TagHelper with the below code
<kenai-date asp-for="DateValue" />
'DateValue' is a public property on the page, when first rendering the page the value of DateValue is correctly visible in the TagHelper Input element, if I force an OnPost, the value is removed.
I have applied the same to a standard input element with asp-for set and that works fine so suspect I am missing something in my TagHelper.
Asp.net core bind model data with name attribute.You use a custom tag helper,so it will get html like <input value="xxx">.So when form post,you cannot bind model data with name attribute,and when return Page in OnPost handler,model data is null.You need to add name attribute to <kenai-date asp-for="DateValue" />.Here is a demo:
TestCustomTagHelper.cshtml:
<form method="post">
<kenai-date asp-for="DateValue" name="DateValue" />
<input type="submit" />
</form>
TestCustomTagHelper.cshtml.cs:
public class TestCustomTagHelperModel : PageModel
{
[BindProperty]
public string DateValue { get; set; }
public void OnGet()
{
DateValue = "sss";
}
public IActionResult OnPost()
{
return Page();
}
}
result:

passing blazor parameters to another page

I have been trying to pass parameters trough another page and this works, however i'm not getting what I desired and it has probably to do with what i pass.
The first thing i pass is a name but includes spaces and special character, the second thing i pass is a web link
how i send it:
<div class="col-sm-4">
<h3>Programming</h3>
#if (programming == null)
{
<p><em>Loading...</em></p>
}
else
{
foreach (var program in programming)
{
#program.Name
<br />
}
}
</div>
where it goes to
#page "/CourseDetails"
#using Portfolio.Models;
#using Portfolio_Frontend.Data;
#using Microsoft.AspNetCore.WebUtilities
#inject NavigationManager NavigationHelper
<h3>CourseDetails</h3>
#if (Name == null)
{
<p><em>Loading...</em></p>
}
else
{
<p>#Name</p>
}
#code {
public string Name { get; set; }
protected override void OnInitialized()
{
var uri = NavigationHelper.ToAbsoluteUri
(NavigationHelper.Uri);
if (QueryHelpers.ParseQuery(uri.Query).
TryGetValue("name", out var name))
{
Name = name.First();
}
}
}
i tried parameters as well and now tried query string gives the same result.
the name it should pass in this particular case is: C# Intermediate: Classes, Interfaces and OOP
What i get is only 'C' I assume because it is not able to translate the #.
is there a way to pass literal strings?
where it goes to: https://localhost:5105/CourseDetails/?name=C#%20Intermediate:%20Classes,%20Interfaces%20and%20OOP
this seems right to me.
Minor correction of URL syntax methodology
You have:
#program.Name
Which has a URL of /CourseDetails/?name=C#
Normally, you would do either
/CourseDetails/C#
/CourseDetails?name=C#
Except, Blazor doesn't explicitly support optional route parameters (/CourseDetails?name=C#)
REF: https://blazor-university.com/routing/optional-route-parameters/#:~:text=Optional%20route%20parameters%20aren%E2%80%99t%20supported%20explicitly%20by%20Blazor,,then%20replace%20all%20references%20to%20currentCount%20with%20CurrentCount.
It looks as though you can keep the optional query parameters and fiddle with the QueryHelpers.ParseQuery() I don't quite buy into that but if you want to keep going that route check out this post by #chris sainty
Link: https://chrissainty.com/working-with-query-strings-in-blazor/
I would much rather create a new model (DTO) that knows exactly how to display the CourseDetails name in a URL encoded fashion for the link, and the display name for the user.
public class ProgramModel
{
private readonly string name;
public ProgramModel(string name)
{
this.name = name;
}
public string DisplayName => name;
public string RelativeUrl => HttpUtility.UrlEncode(name);
}
And when we need to render the links on the 'Courses' page, it would look like this:
#page "/courses"
#using BlazorApp1.Data
<div class="col-sm-4">
<h3>Programming</h3>
#foreach (var program in programming)
{
#program.DisplayName
<br />
}
</div>
#code {
public IEnumerable<ProgramModel> programming { get; set; }
protected override void OnInitialized()
{
programming = new List<ProgramModel>()
{
new ProgramModel("Rust Things"),
new ProgramModel("JavaScript Things"),
new ProgramModel("C# Things")
};
}
}
And finally, when displaying the CourseDetails page, we can simply decode the name from the URL with the same utility that encoded the string in the first place, instead of guessing whether or not it's the apps fault, or the browsers fault that the '#' is not getting encoded properly to '%23'
#page "/CourseDetails/{Name}"
#inject NavigationManager NavigationHelper
#using System.Web
<h3>CourseDetails</h3>
<p>#HttpUtility.UrlDecode(Name)</p>
#code {
[Parameter]
public string Name { get; set; }
}
I recommend letting go of the idea of navigating from page to page, and using components:
<div>
#if (SelectedItem is not null)
{
<MyResultsPage SelectedProgramClass=#SelectedItem />
}
</div>
#code
{
ProgramClass SelectedItem {get; set;}
void SomeWayToSelectMyItem(ProgramClass newSelection){
SelectedItem = newSelection;
StateHasChanged();
}
}
Then in your display page, MyResultsPage.blazor
<div>
<div>#SelectedProgramClass.name</div>
. . .
</div>
#code {
[Parameter]
ProgramClass SelectedProgramClass{get; set;}
}
<MyResultsPage> will not show up in any way on the client, or even be initialized, until you've assigned something to SelectedProgramClass.

Microsoft.AspNetCore.Components.Forms.InputRadioGroup` does not support the type xxx

I want to use radio group in blazor so after implementing edit form and select one of the radio button I got this error :
Microsoft.AspNetCore.Components.Forms.InputRadioGroup`1[EGameCafe.SPA.Models.GameModel] does not support the type 'EGameCafe.SPA.Models.GameModel'.
here is my edit form :
<EditForm Model="ViewModel" OnValidSubmit="HandleCreateGroup">
#if (ViewModel.Games.List.Any())
{
<InputRadioGroup Name="GameSelect" #bind-Value="Gamemodelsample">
#foreach (var game in ViewModel.Games.List)
{
<InputRadio Value="game" />
#game.GameName
<br />
}
</InputRadioGroup>
}
</EditForm>
#code{
public GameModel GameModelSample { get; set; } = new();
}
and GameModel is :
public class GameModel
{
public string GameId { get; set; }
public string GameName { get; set; }
}
The InputRadioGroup, like other Blazor components, supports only a limited amount of types like String or Int32. You had the right idea, but unfortunately, you run into a kind of limitation of Blazor.
You could try to create a wrapper field.
private String _selectedGameId = "<Your Default Id>";
public String SelectedGameId
{
get => _selectedGameId;
set
{
_selectedGameId = value;
// Set the property of the ViewModel used in your Model Property of the EditContext or any other property/field
ViewModel.SelectedGame = ViewModel.Games.List?.FirstOrDefault(x => x.GameId == value);
}
}
Use the property SelectedGameId as the bind value of the InputRadioGroup component.
<InputRadioGroup Name="GameSelect" #bind-Value="SelectedGameId" >
#foreach (var game in ViewModel.Games.List)
{
<InputRadio Value="game.GameId" />
#game.GameName
<br />
}
</InputRadioGroup>
As an alternative, you can create a custom component that inheriting from InputRadioGroup to create a kind of GameBasedInputRadioGroup. If you are interested I can post a sample.
Because in your code #bind-Value="Gamemodelsample",you are trying to bind GameName(string) to Gamemodelsaple(object), which will cause type mismatch problems.
You only need to modify your code to:
#bind-Value="GameModelSample.GameName"