This Section of the Docs describes, how to display Validation Messages.
<ValidationMessage For="() => Parameters.PropertyA"></ValidationMessage>
How can the ValidationMessage.For Property be set dynamically?
Since For is of type Expression<Func<TValue>>, I want to pass a Func instead, but this doesn't compile:
[Parameter]
public Func<string> PropertyLocator { get; set; }
<ValidationMessage For="PropertyLocator"></ValidationMessage>
this compiles, but Validation Messages won't be resolved correctly
<ValidationMessage For="() => PropertyLocator"></ValidationMessage>
I also tried to make the Component generic, such that it knows about the Parameters Type:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Components;
public partial class MyComponent<TParam>
{
[Parameter]
public TParam Parameters { get; set; }
[Parameter]
public Func<TReportParam, string> PropertyLocator { get; set; }
}
#using System.Linq.Expressions
#typeparam TParam
<ValidationMessage For="#((Expression<Func<string>>)(() => PropertyLocator(this.Parameters)))"></ValidationMessage>
<MyComponent TParam="MyParameters" Parameters="BindToSomeValue" PropertyLocator="(parameters) => parameters.PropertyA" />
But this leads to the following run-time exception:
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: The provided expression contains a InvocationExpression1 which is not supported.
FieldIdentifier only supports simple member accessors (fields,
properties) of an object. System.ArgumentException: The provided
expression contains a InvocationExpression1 which is not supported.
FieldIdentifier only supports simple member accessors (fields,
properties) of an object. at
Microsoft.AspNetCore.Components.Forms.FieldIdentifier.ParseAccessor[String](Expression`1
accessor, Object& model, String& fieldName) at
Microsoft.AspNetCore.Components.Forms.FieldIdentifier.Create[String](Expression`1
accessor) at
Microsoft.AspNetCore.Components.Forms.ValidationMessage`1[[System.String,
System.Private.CoreLib, Version=5.0.0.0, Culture=neutral,
PublicKeyToken=7cec85d7bea7798e]].OnParametersSet() at
Microsoft.AspNetCore.Components.ComponentBase.CallOnParametersSetAsync()
at
Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
I've created a small sample page.
The model uses DataAnnotations as the validation mechanism.
public class DemoInputModel
{
[Required]
public String PropertyOne { get; set; }
[MinLength(2)]
public String PropertyTwo { get; set; }
[MaxLength(5)]
public String PropertyThree { get; set; }
}
On the page, the model is initialized and set as the edit context. We have three text inputs and a select box. The select box can be used to toggle the validation message. If the value is of the select box is changed, a new expression is assigned to the ValidationMessage.
#using System.ComponentModel.DataAnnotations;
#using System.Linq.Expressions;
#page "/test"
<h1>ValidationMessageTest</h1>
<EditForm Model="_model">
<DataAnnotationsValidator />
<ValidationMessage For="ValidationResolver"></ValidationMessage>
<InputText #bind-Value="_model.PropertyOne" />
<InputText #bind-Value="_model.PropertyTwo" />
<InputText #bind-Value="_model.PropertyThree" />
<InputSelect #bind-Value="SelectedValidationProperty">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</InputSelect>
#*<ValidationSummary />*#
</EditForm>
#code {
private DemoInputModel _model = new DemoInputModel
{
PropertyOne = "Test",
PropertyTwo = "42",
PropertyThree = "Math.PI",
};
private String _selectedValidationProperty;
public String SelectedValidationProperty
{
get => _selectedValidationProperty;
set
{
_selectedValidationProperty = value;
ChangeValidator(value);
}
}
public Expression<Func<String>> ValidationResolver { get; set; }
protected override void OnInitialized()
{
SelectedValidationProperty = "1";
base.OnInitialized();
}
public void ChangeValidator(String value)
{
switch (value)
{
case "1":
ValidationResolver = () => _model.PropertyOne;
break;
case "2":
ValidationResolver = () => _model.PropertyTwo;
break;
case "3":
ValidationResolver = () => _model.PropertyThree;
break;
default:
break;
}
}
}
Did you mean something like this? It gets slightly more complicated if your model doesn't have only strings, like in the example. A "quick" workaround could be to have an Expression for each possible type.
Under the hood, the expression is used to create a FieldIdentifier. The FieldIdentifier is then used to get the corresponding property/field from the EditContext to check the validation status. Hence, you are constrained in what to choose for the expression. The error message FieldIdentifier only supports simple member accessors (fields, properties) of an object gives a good indication of this limitation.
I want to pass a Func instead
Why? If there isn't a specific reason why you should pass Func<TValue> instead of Expression<Func<TValue>>, just have the parameter
[Parameter]
public Expression<Func<string>> PropertyLocator { get; set; }
If you want only a Func<> because you are going to reuse it for something else other than the For parameter of ValidationMessage, you can take a look at Extracting Func<> from Expression<> to get a Func<> from the Expression<Func<string>> PropertyLocator.
If you really want to pass a Func<>, maybe you will get some problems to transform when trying to convert a .net Func to a .net Expression<Func>.
After some research I stumbled about the following blazor feature:
The holy trinity of blazor bindings
Read more about it here.
In short, if a [Parameter] is bound with the follwoing syntax...
<MyComponent #bind-Value="My.Binding.Path" />
... it not only supports two-way bindings, but it also sets a locator expression.
[Parameter]
public string Value { get; set; }
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
[Parameter]
public Expression<Func<string>> ValueExpression { get; set; }
you may use any type, instead of string
since the value of the ValueExpression is set automatically, you can use this behavior to display the validation message for the bound property. Simply add the ValidationMessage Component to your component with the expression.
<ValidationMessage For="ValueExpression" />
A little extra
If you're building a Component that supports Validation (which at this point, I assume you are). The following might also be interesting for you.
Not only can you use the holy trinity to display validationmessages, but also to create Components supporting validation. There are many articles covering this topic.
In short:
Build your component
Notify field changes on the EditContext whenever needed
To make the above created MyComponents Value Property support validation, just follow these steps.
Define a CascadingParameter EditContext, this gets the current EditContext, usually from the EditForm Component. Also note that the EditContext may not be set, if there's no CascadingValue. For example if the Component isn't placed inside an EditForm:
[CascadingParameter]
public EditContext? EditContext
Define a property to store a FieldIdentifier and set it when parameters are set.
public FieldIdentifier? FieldIdentifier { get; private set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
await base.SetParametersAsync(parameters);
if (this.EditContext != null && this.DateExpression != null && this.FieldIdentifier?.Model != this.EditContext.Model)
{
this.FieldIdentifier = Microsoft.AspNetCore.Components.Forms.FieldIdentifier.Create(this.DateExpression);
}
}
Trigger the validation for the Field whenever you need (usually after the invocation of ValueChanged):
this.Value = value;
this.ValueChanged.InvokeAsync(this.Value);
if (this.FieldIdentifier?.FieldName != null)
{
this.EditContext?.NotifyFieldChanged(this.FieldIdentifier!.Value);
}
Related
I am attempting to create a neat, reusable checkbox list for enums with the [flags] attribute in Razor Pages with .NET Core 7.
I do not know which worflow to use - Partial, TagHelpers, ViewComponent or any others (or a combination), neither how I might apply these tools (having no experience creating any of them) to create a clean, efficient and reusable tool/helper.
The code below works, but it is not particularly reusable - for example, if I wanted to change the html so the label element became a parent of the checkbox input, I will need to change this in every instance of the 'cut and pasted' cshtml code.
In addition, the call to the helper function MyHtmlHelpers.EnumToCheckboxList<Urgency>(nameof(TransportReferral), nameof(TransportReferral.Urgency), TransportReferral?.Urgency) seems verbose and inefficient when compared to TagHelpers. Instead, it would be ideal to be able to access all these arguments with a single reference - in a similar way the TagHelpers do with the asp-for attribute, but I do not know how this might be achieved.
public static partial class MyHtmlHelpers
{
public static IEnumerable<CheckboxListItem> EnumToCheckboxList<TEnum>(string? modelName, string propertyName, TEnum? enumValue) where TEnum : struct, Enum
{
string name = string.IsNullOrEmpty(modelName)
? propertyName
: modelName + '.' + propertyName;
string idPrefix = name.Replace('.', '_');
return Enum.GetValues<TEnum>().Select(e =>
{
var eStr = e.ToString();
var eInt = Convert.ToInt32(e).ToString();
// ignoring DisplayAttribute.Name
return new CheckboxListItem
{
Display = typeof(TEnum).GetMember(eStr)[0]
.GetCustomAttributes<DescriptionAttribute>(false)
.FirstOrDefault()?
.Description ?? SplitCamelCase(eStr),
IsChecked = enumValue.HasValue && enumValue.Value.HasFlag(e),
Value = eInt,
Name = name,
Id = idPrefix + '_' + eInt,
};
}).ToList();
}
public static string SplitCamelCase(string input)
{
return lowerUpper().Replace(input, "$1 $2");
}
[GeneratedRegex("([a-z])([A-Z])", RegexOptions.CultureInvariant)]
private static partial Regex lowerUpper();
}
public class CheckboxListItem
{
public string Display { get; set; }
public string Value { get; set; }
public string Name { get; set; }
public string Id { get; set; }
public bool IsChecked { get; set; }
}
consumed in a cshtml page like so:
#foreach (var e in MyHtmlHelpers.EnumToCheckboxList<Urgency>(nameof(TransportReferral), nameof(TransportReferral.Urgency), Model.TransportReferral?.Urgency))
{
<div class="form-check form-check-inline">
<input type="checkbox"
name="#e.Name"
id="#e.Id"
checked="#e.IsChecked"
value="#e.Value">
<label class="form-check-label" for="#e.Id">
#e.Display
</label>
</div>
}
So in summary, is there a way to refactor the above code, taking advantage of Razor pages tools, to make the cshtml markup more reusable and also allow the full name TransportReferral.Urgency and its value to be passed cleanly to the tool with a single argument, similarly to (or in the same way) the asp-for attribute does for taghelpers?
Essentially what you want to do is replicate the SelectTagHelper with enum support, except you want to render checkboxes instead of option elements. Given that, I would start with the source code for the SelectTagHelper
https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.TagHelpers/src/SelectTagHelper.cs
and the GetEnumSelectList helper https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.ViewFeatures/src/HtmlHelper.cs#L398
I have a EditForm in a Blazor Server application and i want to check if an InputText value is in a list. How can i pass the list to compare from my UI to the Validator class for comparison?
I have tried comparing the #bind-Value in line and encapsulating the validation message but it skips over the validation message when the encapsulating function tests true.
<EditForm Model="#resourceToBeCreated">
<FluentValidationValidator ValidatorType=typeof(ResourceValidator)/>
#if (resourcesSortedCollection.FirstOrDefault(x => x.Name == resourceToBeCreated.Name) != null)
{
<CustomValidationMessage For="() => resourceToBeCreated.Name" />
}
<InputTextOnInput #bind-Value="#resourceToBeCreated.Name" class="form-control" placeholder="Name..." />
</EditForm>
I can obviously do this or something similar in the #code section but i dont get the validation popup on inupt.
So the question is, how can i pass this list to the Validator class for comparison?
EDIT 1:
InputTextOnInput component:
#inherits InputText
<input #attributes="AdditionalAttributes"
class="#CssClass"
value="#CurrentValue"
#oninput="EventCallback.Factory.CreateBinder<string>(this, __value => CurrentValueAsString = __value, CurrentValueAsString)" />
EDIT 2:
A potential workaround while still utilising fluent validation.
1, add new property to the model :
public List<string> ResourceNames { get; set; }
2, when a new resource is created in the browser update that property in the model
resourceToBeCreated.ResourceNames = resourcesSortedCollection.Select(x => x.Name).ToList();
3, write rule in fluent validation
RuleFor(x => x.Name).Null().When(x => x.ResourceNames.Contains(x.Name)).WithMessage("Duplicate resource name");
Not sure if this is the best way to do it (code smell?) but it works for now. Either way i have to create a list of strings which contains all the resource names. If there is a more direct way to pass the resourcesSortedCollection object to the validator id like to understand.
First an inherited InputText control. This overrides TryParseValueFromString and does the validation there.
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorApp1.Pages;
public class InputTextValidated : InputText
{
[Parameter, EditorRequired] public IEnumerable<string>? CheckList { get; set; }
{ get; set; }
protected override bool TryParseValueFromString(string? value, out string? result, [NotNullWhen(false)] out string? validationErrorMessage)
{
result = null;
validationErrorMessage = null;
var isValid = this.CheckList is not null
&& this.CheckList.Any(item => item.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (isValid)
result = value;
else
validationErrorMessage = "You must enter a value in the validation list";
return isValid;
}
}
And a test page:
#page "/"
<PageTitle>Index</PageTitle>
<EditForm Model=this.model>
<DataAnnotationsValidator />
<InputTextValidated class="form-control" CheckList=Countries #bind-Value="#this.model.Country" />
<ValidationSummary />
</EditForm>
<div>
Country : #model.Country
</div>
#code {
private DataModel model = new DataModel();
private List<string> Countries = new List<string> { "UK", "Spain" };
public class DataModel
{
public string? Country;
}
}
As an alternative you could use/build an Input List control.
How Validation works
Validation data is held in ValidationMessageStore's associated with an EditContext. A ValidationMessageStore is a composite collection of key/value pairs:
the field defined as a FieldIdentifier [the model as an object and the field name as a string]
the validation message as string.
Each validation provider has it's own message store and can clear and add messages to it. It only has write/delete access to it's own message store. Providers get the EditContext cascaded by the EditForm, create a message store associated with the EditContext and logs messages to and clears messages from that store. FluentValidationValidator, DataAnnotationsValidator, any InputBase control or classes you write that interact with the EditContext are providers with message stores associated with the EditContext.
ValidationSummary and ValidationMessage are consumers. They interact with the message stores associated with a EditContext via the cascaded EditContext. All the messages are available as read only. ValidationMessage constructs a FieldIdentifier from the For expression to filter the messages.
I thought id throw an answer up as i stumbled upon one while doing something else and it may help another.
You can pass the values to validate against into the validator when it is instantiated. In this case pass in a list of BaseResourceMoldes into the ResouceValidator via a constructor. As the list wont change between instantiation and validation this is suitable.
You then use the Must extension which will pass the parameter you are validating into a called function to test for bool. In this case .Must(IsUnique) passes x.Name into IsUnique(string arg1) and returns a bool.
Syntax might be slightly different to examples above as the code base will have changed between then and now but the concept is the same.
The class with the form to be validated:
[Parameter] public List<BaseResourceModel> Resources { get; set; }
ResourceValidator resourceValidator;
protected override void OnInitialized()
{
resourceValidator = new ResourceValidator(Resources);
}
And then the ResourceValidator Class:
private List<BaseResourceModel> _resources;
private void ResourceValidator(List<BaseResourceModel> resources)
{
_resources = resources;
RuleFor(x => x.Name).NotEmpty().Must(IsUnique).WithMessage("Resource name must be unique");
}
private bool IsUnique(string arg1)
{
bool isUnique = true;
foreach (var resource in _resources)
{
if (resource.Name == arg1)
{
isUnique = false;
}
}
return isUnique;
}
I'm assuming you could also do this asynchronously if the list had the potential to change. Fluent validation has async methods.
I have a class MealsQueryInputs that I would like to use as a component parameter with two-way binding capabilities.
All of the demos and sample code I can find are using built-in primitive types and never a class. I can get the MS demos to work but I cannot get binding to a class to work. Is it even possible to do this?
My component FilterSortOptions.razor:
using WhatIsForDinner.Shared.Models
<MudCheckBox Checked="#QueryInputs.Favorite"
Color="Color.Inherit"
CheckedIcon="#Icons.Material.Filled.Favorite"
UncheckedIcon="#Icons.Material.Filled.FavoriteBorder"
T="bool"/>
<MudRating SelectedValue="#QueryInputs.Rating"/>
<MudButton OnClick="#(async () => await OnPropertyChanged())">Apply</MudButton>
#code {
[Parameter]
public MealsQueryInputs QueryInputs { get; set; }
[Parameter]
public EventCallback<MealsQueryInputs> QueryInputsChanged { get; set; }
private async Task OnPropertyChanged()
{
await QueryInputsChanged.InvokeAsync(QueryInputs);
}
}
As MrC said, you should avoid directly binding to the data being supplied as a parameter.
Here is a simple working sample (not MudBlazor) to show the concept
https://blazorrepl.telerik.com/QQEnQjaO54LY3MYK35
You bind to a local variable/property and try not to modify the incoming data directly.
MyComponent
<h1>MyComponent</h1>
<label for="choice">Choose</label>
<input id="choice" type="checkbox" #bind-value=localValue />
#code
{
bool localValue
{
get => Data.SomeChoice;
set {
if (value != localValue)
{
localData = Data with { SomeChoice = value };
InvokeAsync(ValueChanged);
}
}
}
ComplexObject localData;
[Parameter] public ComplexObject Data { get; set; }
[Parameter] public EventCallback<ComplexObject> DataChanged { get; set; }
Task ValueChanged() => DataChanged.InvokeAsync(localData);
}
ComplexObject
public record ComplexObject(bool SomeChoice, string SomeText);
Main
#code
{
ComplexObject data = new(false,"");
}
<MyComponent #bind-Data=data />
You have chosen #data.SomeChoice
Here is how you can bind class objects to a custom razor component
This is FilterSortOptions component
<div>
<label>Rating:</label>
<input type="text" value=#QueryInputs.Rating #oninput=#(val=> {
QueryInputs.Rating=val.Value.ToString();
QueryInputsChanged.InvokeAsync(QueryInputs);
}) />
</div>
<div>
<label>Favourite:</label>
<input type="checkbox" value=#QueryInputs.Rating #onchange=#(val=> {
QueryInputs.Favourite=(bool)val.Value;
QueryInputsChanged.InvokeAsync(QueryInputs);
}) />
</div>
#code {
[Parameter]
public MealsQueryInputs QueryInputs { get; set; }
[Parameter]
public EventCallback<MealsQueryInputs> QueryInputsChanged { get; set; }
}
This is the model to bind, for simplicity Rating is is string type
public class MealsQueryInputs
{
public bool Favourite { get; set; } = false;
public string Rating { get; set; } = "0";
}
Here is the razor page
<h3>Rating: #QueryInputs.Rating</h3>
<h3>Favourite: #QueryInputs.Favourite</h3>
<FilterSortOptions #bind-QueryInputs=#QueryInputs></FilterSortOptions>
#code {
public MealsQueryInputs QueryInputs = new();
}
Updated Answer
Firstly, if your using an object then you are passing around references to the same object. So when you update the object in the sub-component, you're updating the same object the parent is using. You don't need to pass the object back in the callback unless you create a noew copy of it.
Secondly, your not binding the mud controls to the object.
Let's look at your code:
<MudCheckBox Checked="#QueryInputs.Favorite"
Color="Color.Inherit"
CheckedIcon="#Icons.Material.Filled.Favorite"
UncheckedIcon="#Icons.Material.Filled.FavoriteBorder"
T="bool"/>
Checked="#QueryInputs.Favorite" doesn't bind the control to the field. It just sets the initial value.
I think (I don't use Mudblazor and it's a little different from standard Blazor Form Controls) you need to do this:
<MudCheckBox #bind-Checked="#QueryInputs.Favorite"></MudCheckBox>
The same is true for MudRating.
<MudRating #bind-SelectedValue="#QueryInputs.Rating" />
Then the button:
<MudButton OnClick="#(async () => await OnPropertyChanged())">Apply</MudButton>
can be simplified to this. You're wrapping an async method within an async method.
<MudButton OnClick="OnPropertyChanged">Apply</MudButton>
// or
<MudButton OnClick="() => OnPropertyChanged()">Apply</MudButton>
Original Answer
There are a couple of issues here:
QueryInputs is a Parameter and therefore should never be modified by the code within the component. You end up with a mismatch between what the Renderer thinks the value is and what it actually is.
When the parent component renders it will always cause a re-render of any component that is passed a class as a parameter. The Renderer has no way of telling if a class has been modified, so it applies the heavy handed solution - call SetParametersAsync on the component.
A solution is to use a view service to hold the data and events to notify changes. One version of the truth! Search "Blazor Notification Pattern" for examples of how to implement this. I'll post some code if you can't find what you want.
I have a need to use some custom inputs built as razor components on pre-existing views and pages, but can't seem to get it to work using the component tag helper. For example, the component code that I've been testing with (from https://chrissainty.com/creating-bespoke-input-components-for-blazor-from-scratch/) at first results in an exception because ValueExpression ends up being null (no options for binding using the tag helper, from what I can tell). If I then set ValueExpression myself, I end up with a json exception (object cycle detected). I think maybe because the mechanism for moving parameters from the tag helper to the underlying component doesn't support Func<> objects? Not sure.
Am I trying to use the tag helper incorrectly perhaps? I'm using it in other places to render self-contained components (like an entire EditForm), and that seems to be working fine, but how to get it working in this particular use case eludes me :(
Inside .cshtml file I want the control to render in:
<component type="typeof(MyComponent)" render-mode="ServerPrerendered" param-ValueExpression="(Func<string>)(() => LocalProperty)" />
MyComponent.razor
<input class="_fieldCssClasses" value="#Value" #oninput="HandleInput" />
#if (_showValidation) {
<div class="validation-message">You must provide a name</div>
}
#code {
private FieldIdentifier _fieldIdentifier;
private string _fieldCssClasses => EditContext?.FieldCssClass(_fieldIdentifier) ?? "";
private bool _showValidation = false;
[CascadingParameter] private EditContext EditContext { get; set; }
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public Expression<Func<string>> ValueExpression { get; set; }
[Parameter] public bool Required { get; set; }
protected override void OnInitialized() {
_fieldIdentifier = FieldIdentifier.Create(ValueExpression);
}
private async Task HandleInput(ChangeEventArgs args) {
await ValueChanged.InvokeAsync(args.Value.ToString());
if (EditContext != null) {
EditContext.NotifyFieldChanged(_fieldIdentifier);
} else if (Required) {
_showValidation = string.IsNullOrWhiteSpace(args.Value.ToString());
}
}
}
So to resolve this, I just made a use-case-specific wrapper component around the inner component, making sure to also pass in an Id that the inner component will assign to the name and id attributes of the input it's rendering (allows it to "bind" when you submit the form), and then included the wrapper component on the page/view. The wrapper simply satisfies the dependencies of the inner component, which the component tag helper does not provide a mechanism for if the dependency is more complex (like a Func<> object).
MyComponentWrapper.razor:
<div class='purdy'>
<MyComponent Id="#Id" #bind-Value="Value"></MyComponent>
</div>
...
#code {
[Parameter]
public string Id { get; set; }
[Parameter]
public string Value { get; set; }
...
}
MyContainingPage.cshtml:
<component type="typeof(MyComponentWrapper)" render-mode="ServerPrerendered" param-Id="myFormInput" param-Value="LocalProperty" />
...
I used the approach described in this article to create a drop down.
The Model
public class IceCreamFlavor
{
public int Id { get; set; }
public string Name { get; set; }
}
The View Model
public class ViewModel
{
private readonly List<IceCreamFlavor> _flavors;
[Display(Name = "Favorite Flavor")]
public int SelectedFlavorId { get; set; }
public IEnumerable<SelectListItem> FlavorItems
{
get { return new SelectList(_flavors, "Id", "Name");}
}
}
The View
#Html.LabelFor(m=>m.SelectedFlavorId)
#Html.DropDownListFor(m => m.SelectedFlavorId, Model.FlavorItems)
#Html.ValidationMessageFor(m=>m.SelectedFlavorId)
<input type="submit" value="Submit" />
This approach works fine.
Now I want to display a property of the Model on the same view. As an example assume we had the following properties.
public class IceCreamFlavor
{
public int Id { get; set; }
public string Name { get; set; }
public float Price { get; set; }
}
Now underneath the Dropdown I need to display the price as
Price : 15.99
How can I achieve this?
I would rather choose a another solution, since firing ajax for every selected input is useless and consuming.
Using the normal current DropDownListFor in addition with outputting the complete price list to hiding input value. value e.g: '10.0;12.0;...' which every value is than can be taken by simple JavaScript procedure with the option index as the mark for the value you should take.
Constructing a new MyDropDownListFor which will follow as the current but instead of just constructing normal <option>.. it will also add to that html tags the price or whatever additional parameter you want it to display as well. Examples: Here Here Here
No matter what solution you take, it will have to be combined with supporting simple JavaScript method which then renders the Selection and Displaying the Price which already been downloaded.
To render a property off of the model after submitting, you can just break into HTML to display it:
#if (Model.Price != 0.0F)
{
<b>Price #Model.Price.ToString("0.00") </b>
}
To achieve this, add a collection onto the ViewModel:
public class ViewModel
{
private readonly System.Collections.Generic.List<IceCreamFlavor> _flavors;
public ViewModel()
{
// Construct Flavors
}
public List<IceCreamFlavor> AllFlavors
{
get
{
return _flavors;
}
}
[Display(Name = "Favorite Flavor")]
public int SelectedFlavorId { get; set; }
public System.Web.Mvc.SelectList FlavorItems
{
get { return new System.Web.Mvc.SelectList(_flavors, "Id", "Name");}
}
}
Then on the View:
#if (Model.AllFlavors.Any(f => f.Id == Model.SelectedFlavorId))
{
<b>Price #Model.AllFlavors.First(f => f.Id == Model.SelectedFlavorId).Price.ToString("0.00") </b>
}
You could, of course, just expose the selected Flavor as a property on the ViewModel (similar display principle applies). But, the advantage of exposing all the Flavors as a property, is you can easily move to storing this in JavaScript on page and query that, rather than relying on the submit button.
Then you can roll your own drop down onchange events using JavaScript / JQuery to read from this object stored on page. (Or use AJAX to make a call to another action to return the value as needed..)
The solution not exposing all flavors is:
Property on ViewModel:
public IceCreamFlavor SelectedFlavor
{
get
{
return _flavors.FirstOrDefault(f => f.Id == this.SelectedFlavorId);
}
}
Display on View:
#if (Model.SelectedFlavor != null)
{
<b>Price #Model.SelectedFlavor.Price.ToString("0.00") </b>
}