Business Validation with ObjectGraphDataAnnotationsValidator with complex type model - blazor-server-side

In my blazor server application i have done business validation based on this
#page "/"
#inherits IndexBase
<EditForm Model="#MyModel" OnValidSubmit="#HandleValidSubmit">
<ObjectGraphDataAnnotationsValidator />
<CustomValidator #ref="customValidator" />
<ValidationSummary />
<div>
<InputText #bind-Value="MyModel.Customer.Name"></InputText>
<ValidationMessage For="()=>MyModel.Customer.Name" />
</div>
<div>
<InputNumber #bind-Value="MyModel.Customer.Age"> </InputNumber>
<ValidationMessage For="()=>MyModel.Customer.Age" />
</div>
<button Text="Save" type="submit">Get Result</button>
</EditForm>
and its base class conatains
public class IndexBase:ComponentBase
{
protected MyModel MyModel { get; set; } = new MyModel();
protected CustomValidator customValidator;
protected void HandleValidSubmit()
{
customValidator.ClearErrors();
var errors = new Dictionary<string, List<string>>();
if ( MyModel.Customer.Name.ToLower().StartsWith("a"))
{
errors.Add("Customer.Age", new List<string>() { "Age should be greater than 10" });
}
if (errors.Count() > 0)
{
customValidator.DisplayErrors(errors);
}
else
{
// Process the form
}
}
}
and MyModel looks like
public class MyModel
{
[ValidateComplexType]
public Customer Customer { get; set; } = new Customer();
}
public class Customer
{
[Required]
public string Name { get; set; }
public int Age { get; set; }0
}
and my CustomValidator looks like below code (from here)
public class CustomValidator : ComponentBase
{
private ValidationMessageStore messageStore;
[CascadingParameter]
private EditContext CurrentEditContext { get; set; }
protected override void OnInitialized()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException(
$"{nameof(CustomValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. " +
$"For example, you can use {nameof(CustomValidator)} " +
$"inside an {nameof(EditForm)}.");
}
messageStore = new ValidationMessageStore(CurrentEditContext);
CurrentEditContext.OnValidationRequested += (s, e) =>
messageStore.Clear();
CurrentEditContext.OnFieldChanged += (s, e) =>
messageStore.Clear(e.FieldIdentifier);
}
public void DisplayErrors(Dictionary<string, List<string>> errors)
{
foreach (var err in errors)
{
FieldIdentifier x = CurrentEditContext.Field(err.Key);
messageStore.Add(x, err.Value);
}
CurrentEditContext.NotifyValidationStateChanged();
}
public void ClearErrors()
{
messageStore.Clear();
CurrentEditContext.NotifyValidationStateChanged();
}
}
When i click submit button validation code works, but does not adding modified invalid class to input control. So this does not display error message and red border to control

I can do like this..
protected void HandleValidSubmit()
{
customValidator.ClearErrors();
var errors = new Dictionary<FieldIdentifier, List<string>>();
if ( MyModel.Customer.Name.ToLower().StartsWith("a"))
{
errors.Add(new FieldIdentifier(MyModel.Customer, "Name"), new
List<string> { "Age should be greater than 10" });
}
if (errors.Count() > 0)
{
customValidator.DisplayErrors(errors);
}
else
{
// Process the form
}
}
and changed the parameter type in DisplayErrors method like this,
public void DisplayErrors(Dictionary<FieldIdentifier, List<string>> errors)
{
foreach (var err in errors)
{
messageStore.Add(err.Key, err.Value);
}
CurrentEditContext.NotifyValidationStateChanged();
}

Related

DynamicComponent send value from child to the parent

I have a class that represents the necessary data for creating a DynamicComponent:
public class ComponentMetadata
{
public string? Name { get; set; }
public Dictionary<string, object> Parameters { get; set; } =
new Dictionary<string, object>();
}
I have a custom table component that uses ComponentMetadata to add some dynamic component to the table:
TableTemplate.razor:
<table class="table">
<thead>
<tr>#TableHeader</tr>
</thead>
<tbody>
#foreach (var RowTemplateConfig in RowTemplateConfigs)
{
if (RowTemplateConfig.DataItem is not null)
{
<tr>
<td> #RowTemplateConfig.DataItem </td>
#if(RowTemplateConfig.ComponentMetadata is not null)
{
<td>
<DynamicComponent
Type= RowTemplateConfig.ComponentMetadata.Name
Parameters= RowTemplateConfig.ComponentMetadata.Parameters />
</td>
}
</tr>
}
}
</tbody>
</table>
#code {
public class RowTemplateConfig
{
public ComponentMetadata ComponentMetadata { get; set;}
public string DataItem { get; set; }
}
[Parameter]
public RenderFragment? TableHeader { get; set; }
[Parameter]
public List<RowTemplateConfig> RowTemplateConfigs {get; set;}
}
I have also other custom components for example Button custom component:
MyButton.razor
<button type="button"
class="#Style"
#onclick="BtnClick">
#Text
</button>
#code {
[Parameter]
public string Style { get; set; }
[Parameter]
public string Text { get; set; }
[Parameter]
public EventCallback<object> OnBtnClick { get; set; }
private async Task BtnClick(object item)
{
await OnBtnClick.InvokeAsync(item);
}
}
I want to use my TableTemplate.razor in some razor page:
pets1.razor
#page "/pets1"
<h1>Pets</h1>
<TableTemplate RowTemplateConfigs =RowTemplateConfigs TItem =string>
<TableHeader>
<th>Name</th>
</TableHeader>
</TableTemplate>
#code {
private List<RowTemplateConfig> RowTemplateConfigs = new()
{
RowTemplateConfig = new ()
{
DataItem = "Bigglesworth",
ComponentMetadata = new()
{
Name= typeof(MyButton).
Parameters = new Dictionary<string, object>()
{
{ nameof(MyButton.Style), "btn btn-outline-success" }
{ nameof(MyButton.OnBtnClick), EventCallback.Factory.Create<object>(this, OnTestClick)},
{ nameof(MyButton.Text), "Hi"}
}
}
},
RowTemplateConfig = new ()
{
DataItem = "Saberhagen",
ComponentMetadata = new()
{
Name= typeof(MyButton).
Parameters = new Dictionary<string, object>()
{
{ nameof(MyButton.Style), "btn btn-outline-success" }
{ nameof(MyButton.OnBtnClick), EventCallback.Factory.Create<object>(this, OnTestClick)},
{ nameof(MyButton.Text), "Hi"}
}
}
},
//rest of the lest ...
}
private async Task OnTestClick(object sender)
{
// I want to catch the name of the pet here when user click on the button
await Task.Delay(20);
}
}
My Question:
When user clicks on MyButton in TableTemplate in pets1.razor I want to catch the element (name of pet) in the event OnTestClick.
Thanks and sorry for long question, I appreciate any suggestion.
You need to add a Pet Name parameter to the button component, load it and then submit the property back on the callback event.
Here's a modified version of your code (with a few typo and type fixes). Note: I code with Nullable enabled.
ComponentMetadata
public class ComponentMetadata
{
public Type Name { get; set; } = default!;
public Dictionary<string, object> Parameters { get; set; } =
new Dictionary<string, object>();
}
MyButton.razor
<button type="button" class="#Style" #onclick=BtnClick>
#Text
</button>
#code {
[Parameter] public string? Style { get; set; }
[Parameter] public string? Name { get; set; }
[Parameter] public string? Text { get; set; }
[Parameter] public EventCallback<string> OnBtnClick { get; set; }
private async Task BtnClick()
=> await OnBtnClick.InvokeAsync(this.Name);
}
And then the rest of the code rolled into my test page:
#page "/"
#foreach (var component in myDynamicComponents)
{
<DynamicComponent Type="#component.ComponentMetadata.Name" Parameters=component.ComponentMetadata.Parameters />
}
<div class="p-2 m-2">
<strong>Message:</strong> #message
</div>
#code {
private string message = string.Empty;
private RowTemplateConfig GetRowTemplate(string petName)
=> new RowTemplateConfig
{
DataItem = petName,
ComponentMetadata =
new ComponentMetadata()
{
Name = typeof(MyButton),
Parameters = new Dictionary<string, object>()
{
{ nameof(MyButton.Style), "btn btn-outline-success ms-1" },
{ nameof(MyButton.Name), petName },
{ nameof(MyButton.OnBtnClick), EventCallback.Factory.Create<string>(this, OnTestClick)},
{ nameof(MyButton.Text), $"Hi {petName}"}
}
}
};
private List<RowTemplateConfig> myDynamicComponents => new List<RowTemplateConfig> {
GetRowTemplate("Ginger"),
GetRowTemplate("Tiger"),
GetRowTemplate("Felix")
};
private async Task OnTestClick(string name)
{
await Task.Delay(20);
message = $"{name} Clicked at {DateTime.Now.ToLongTimeString()}";
}
public class RowTemplateConfig
{
public ComponentMetadata ComponentMetadata { get; set; } = default!;
public string? DataItem { get; set; }
}
}

Update UI when model bound to component is changed in Blazor

I've created a simplified version of my code to demostrate the issue I'm having. I have a custom Blazor component, TestComponent.razor:
#{ int index = 0; }
#foreach (var option in _value)
{
<input id="#index" type="checkbox" #bind="#option.Selected" /><label for="#index">#option.Name</label>
index++;
}
#code {
[Parameter] public EventCallback<TestModel[]> ValueChanged { get; set; }
private TestModel[] _value;
[Parameter] public TestModel[] Value
{
get => _value;
set
{
if (_value == value) return;
_value = value;
ValueChanged.InvokeAsync(value);
}
}
}
I have a basic model, TestModel.cs:
public class TestModel
{
public string Name { get; set; }
public bool Selected { get; set; }
}
My page simply binds an array of my TestModel to the TestComponent and shows all selected options:
#page "/"
<TestComponent #bind-Value="testOptions" />
#foreach (var option in testOptions)
{
if (option.Selected)
{
<p>#option.Name</p>
}
}
#code {
private TestModel[] testOptions = new TestModel[]
{
new TestModel() { Name = "Amy", Selected = true },
new TestModel() { Name = "Bob", Selected = false }
};
}
On loading the page I get a list of selected options. When I check/uncheck any of the options I can see that the model updates (I added a button which writes the model to the console) but the UI is not updated. Please could someone advise what it is I'm missing to get the UI to update the list of selected options?
Thanks to Jason for his help in getting me to a solution for this. I got this working by adding StateHasChanged() to a newly created OnChangeAsync method. This requires a bit of fiddling around because my model is an array and each checkbox changes just one member of that array.
New TestComponent.razor:
#for (int i = 0; i < _value.Length; i++)
{
int index = i;
<input id="#index" type="checkbox" checked="#_value[index].Selected" #onchange="async args => { await OnChangeAsync(args, index); }" /><label for="#index">#_value[index].Name</label>
}
#code {
[Parameter] public EventCallback<TestModel[]> ValueChanged { get; set; }
private TestModel[] _value;
[Parameter] public TestModel[] Value
{
get => _value;
init => _value = value;
}
public async Task OnChangeAsync(ChangeEventArgs args, int index)
{
_value[index].Selected = (bool)args.Value;
await ValueChanged.InvokeAsync(_value);
StateHasChanged();
}
}

dropdown population with viewmodel asp.net mvc

i am new in MVC. so when see code to understand then some time confusion occur. here i am giving a code. so please see the code first.
public class ProductViewModel
{
public int ID { set;get;}
public string Name { set;get;}
}
public class OrderViewModel
{
private List<ProductViewModel> _products;
public int OrderNumber { set; get; }
public List<ProductViewModel> Products
{
get
{
if (_products == null)
{
_products = new List<ProductViewModel>();
_products.Add(new ProductViewModel { ID = 1, Name = "Ketchup" });
_products.Add(new ProductViewModel { ID = 1, Name = "Mustard" });
_products.Add(new ProductViewModel { ID = 1, Name = "Relish" });
_products.Add(new ProductViewModel { ID = 1, Name = "Mayo" });
}
return _products;
}
}
public int SelectedProductId { set;get;}
}
public ActionResult Order()
{
OrderViewModel orderVM = new OrderViewModel();
return View(orderVM);
}
#model ORderViewModel
#using (Html.BeginForm())
{
<p>
#Html.DropDownListFor(x => x.SelectedProductId , new SelectList(Model.Products, "Value", "Text"), "-- Select Product--")
</p>
}
my question is can i place this code public int SelectedProductId { set;get;} in ProductViewModel instead of OrderViewModel.
if it is possible then what to change in code and in view html ?

Multiple forms on mvc4 page, submit one form

I'm facing an issue with listing multiple forms, incl submit buttons.
When clicking the first button it posts correctly, but when I click the second submit button it submits an empty collection...
Below is my code:
Index.cshtml
#model MVCFormsSubmitting.Models.FormsRepository
#{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Index</h2>
#for (int i = 0; i < Model.UserInfo.Count; i++)
{
using (Html.BeginForm("Index", "Forms", FormMethod.Post, new {name = "Form" + #i}))
{
<b>#Html.EditorFor(m => m.UserInfo[i].Name)</b>
<input name="button #i" type="submit" class="left btn btn-primary" value="Ret navne">
<br/>
}
}
Formsrepository.cs
namespace MVCFormsSubmitting.Models
{
public class Info
{
public int Id { get; set; }
public string Name { get; set; }
public string Age { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
}
public class FormsRepository
{
public FormsRepository ()
{
this.UserInfo = new List<Info>();
}
public List<Info> UserInfo { get; private set; }
public async Task Load()
{
this.UserInfo.Clear();
UserInfo = await LoadUsers();
}
public async static Task<List<Info>> LoadUsers()
{
List<Info> info = new List<Info>();
info.Add(new Info(){
Age = "32,",
Email = "mail#mail.com",
Name = "John Doe",
Phone = "123456749",
Id = 0
});
info.Add(new Info()
{
Age = "36",
Email = "exmaple#example.com",
Name = "Jane Doe",
Phone = "987654321",
Id = 1
});
return info;
}
}
}
FormsController.cs
public class FormsController : Controller
{
//
// GET: /Forms/
public ActionResult Index()
{
FormsRepository.Load();
return View(FormsRepository);
}
[HttpPost]
public ActionResult Index(FormsRepository text)
{
return RedirectToAction("Index");
}
private static FormsRepository _repository;
public static FormsRepository FormsRepository
{
get
{
if (_repository == null)
{
_repository = new FormsRepository();
}
return _repository;
}
}
}
When setting a breakpoint at the HttpPost action in the Formscontroller you will see when clicking the submit button on the first will send 1 item, but when clicking the second button the item is null ...
please help :)
I did some changes to make this code work!
1) Change the controller/view to Render each UserInfo as a strongly typed partial view:
// This method will receive an info to process, so the model binder will build the model back in the server correctly
[HttpPost]
public ActionResult Index(Info userInfo)
{
return RedirectToAction("Index");
}
// This method will render a partial view for a user info
[ChildActionOnly]
public ActionResult GetUserInfo(Info userInfo)
{
return PartialView("_UserInfo", userInfo);
}
2) Change the view to render a partial view for each user info in the repository:
#model MVCFormsSubmitting.Models.FormsRepository
#{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Index</h2>
#for (int i = 0; i < Model.UserInfo.Count; i++)
{
{Html.RenderAction("GetUserInfo", Model.UserInfo[i]);}
}
3) Create a Partial view to render an user info "_UserInfo":
#model MVCFormsSubmitting.Models.Info
#using (Html.BeginForm("Index", "Forms", FormMethod.Post, new { id = "Form" + #Model.Id, name = "Form" + #Model.Id }))
{
<b>#Html.EditorFor(m => m.Name)</b>
<input id="button_#Model.Id" name="button_#Model.Id" type="submit" class="left btn btn- primary" value="Ret navne">
<br/>
}
Try this:
....
using (Html.BeginForm("Index", "Forms", FormMethod.Post, new {name = "Form" + i.ToString()}))
....

DropDownList with possible nested DropDownList in MVC4

I have a set of questions the user can choose from and some of those questions have a secondary list of options to choose from. My goal is to have a drop down list and if you pick one of the options that has items in its SecondaryChoiceList then a second list would appear below the initial dropdown and all of this would be strongly typed and bound to the model upon submission.
I can get the initial list to appear by saying:
#Html.DropDownListFor( x => x.SelectedChoiceId, new SelectList(Model.Choices, "Id", "Name"))
But that has no hooks to the secondary list and I am completely lost as to how I would tie that secondary list back to the model that is returned when I submit the form.
Here's my view model:
public class ExampleViewModel
{
public List<Choice> ChoiceList { get; set; }
public int SelectedChoiceId { get; set; }
public int SelectedAffiliateId { get; set; }
}
Here is what a Choice looks like:
public class Choice
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<SecondaryChoice> SecondaryChoiceList { get; set; }
public Choice()
{
SecondaryChoiceList = new List<SecondaryChoice>();
}
}
And here is my SecondaryChoice object:
public class EligibleAffiliate
{
public int Id { get; set; }
public int EligibilityChoiceId { get; set; }
public string Name { get; set; }
}
If there is anything that I can clear up let me know.
I have tried to keep it as simple as possible.
So, a sample model is given below:
namespace StackOverflow.Models
{
public class Choice
{
public int Id { get; set; }
public string Name { get; set; }
public Choice()
{
Id = 0;
}
public Choice(int id, string name)
{
Id = id;
Name = name;
}
}
}
namespace StackOverflow.Models
{
public class ExampleViewModel
{
public List<Choice> PrimaryChoiceList { get; set; }
public List<Choice> SecondaryChoiceList { get; set; }
public int SelectedChoiceId { get; set; }
public int SelectedAffiliateId { get; set; }
public ExampleViewModel()
{
SelectedChoiceId = 0;
SelectedAffiliateId = 0;
PrimaryChoiceList = new List<Choice>()
{
new Choice(1, "How are you?"),
new Choice(2, "How is the weahter?"),
new Choice(3, "What have you been doing so far?"),
new Choice(4, "What's up man?"),
new Choice(5, "Any news?"),
new Choice(5, "Bla bla bla")
};
SecondaryChoiceList = new List<Choice>()
{
new Choice(1, "How are you dear?"),
new Choice(2, "How is the weahter?"),
new Choice(3, "What have you been doing so far dear?"),
new Choice(4, "What's up man?"),
new Choice(5, "Any romantic news?")
};
}
}
}
Sample controller:
namespace StackOverFlow.Controllers
{
public class SOController : Controller
{
public static ExampleViewModel evm = new ExampleViewModel();
public ActionResult Index()
{
return View(evm);
}
public ActionResult SetSelection(int id)
{
evm.SelectedChoiceId = id;
if (evm.PrimaryChoiceList.Count() > 0)
{
Choice selection = evm.PrimaryChoiceList.ElementAt(id-1);
Choice affiliate = (Choice)evm.SecondaryChoiceList.FirstOrDefault(x => x.Name == selection.Name);
if (affiliate != null)
{
return Content("show");
}
else
{
return Content("hide");
}
}
else
{
return Content("hide");
}
}
}
}
And the web page:
#using StackOverflow2.Models;
#model ExampleViewModel
<script src="#Url.Content("~/Scripts/jquery-1.7.1.min.js")" type="text/javascript"></script>
#{
ViewBag.Title = "Stackoverflow Sample";
}
<h2>Index</h2>
<script type="text/javascript">
// Get the selection and make Ajax Request to the controller, action: SetSelection,
// which in turn may decide whetger you must show or hide the control
function updateSeconadryQuestion(id) {
var xmlhttp;
if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp = new XMLHttpRequest();
}
else {// code for IE6, IE5
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
if (xmlhttp.responseText == 'show')
$('#SecondaryQuestionDropBoxId').show();
else
$('#SecondaryQuestionDropBoxId').hide();
}
}
xmlhttp.open("GET", "/SO/SetSelection?id=" + id, true);
xmlhttp.send();
}
</script>
#Html.DropDownListFor(x => x.SelectedChoiceId, new SelectList(Model.PrimaryChoiceList, "Id", "Name", "Value"), new { id = "PrimaryQuestionDropBoxId", onchange = "updateSeconadryQuestion(value);" })
<div id="SeconadryQuestionDivId">
#Html.DropDownListFor(x => x.SelectedAffiliateId, new SelectList(Model.SecondaryChoiceList, "Id", "Name"), new { id = "SecondaryQuestionDropBoxId" })
</div>