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.
Related
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 .NET 5 application with EntityFramework Core, and I try to link the type of an entity from the drowdown list:
I have a business class
public class Bar : IdEntity
{
public BarType BarType { get; set; }
public class BarType : IdEntity
{
public int Id { get; set; }
public string Name { get; set; }
In order do not use directly Bar in the view, I created a DTO (data transfer object, or just a viewModel), like this:
public class BarDTO : IdEntityDTO
{
public int BarTypeId { get; set; }
public BarType BarType { get; set; }
In controller I do:
public class BarController : IdEntityController<Bar, BarDTO>
{
public override async Task<IActionResult> CreateAsync()
{
var barTypes=await _repository.ListAsync<BarType>();
ViewBag.BarTypes = barTypes;
return await base.CreateAsync();
}
In view
#model MyApp.Web.ViewModels.BarDTO
<select asp-for="BarTypeId"
asp-items="#(new SelectList(ViewBag.BarTypes,
"Id", "Name"))">
<option value="">please select</option>
</select>
my question is how to link the select user choice with the BarType of Bar to create a valid Bar in the Database with corresponding type id.
Your problem involves the so-called model binding in asp.net core. More specifically you want to convert an int Id into an instance of BarType. The form data sent from the client are just string values paired with the corresponding keys. The keys are just like the paths to target the corresponding model property. Because model on the server side may be a complex type with deep properties graph. So the key can be a long dot-separated path. In your case your path is just BarType which basically targets the wrong corresponding Id. The model binder cannot simply convert an int to an instance of BarType. The correct path is BarType.Id. So you can have code like this:
<select asp-for="BarType.Id" asp-items="#(new SelectList(ViewBag.BarTypes, "Id", "Name"))">
<option value="">please select</option>
</select>
That would help the model binder create instances of BarType automatically with the received Id. However we just send the Id so the instances of BarType have just the Id and all the Names are null. Select html element can just hold one value mapped with one property and usually that's the key value. In your case actually the model should not need the BarType.Name at all. When we deal with selectable data, we just need the selected key/id. With that in mind, we are done with the code above.
If you want to receive the BarType.Name as well, I must say that it's wrongly designed. Unless the BarType.Name sent from the client can be edited but in this case obviously it's just a constant like the BarType.Id. When saving data, the Id is what we need to link the entities (establishing the relationship), the other properties don't matter at all and actually can be derived/fetched from the Id.
If you still want to receive the BarType.Name anyway, you have at least 2 options. The first simple one is to declare a not-mapped property containing both Id and Name, constructing the computed value in some way so that you can extract each separate value from it. Here is an example:
public class BarType : IdEntity
{
public int Id { get; set; }
public string Name { get; set; }
//this property should be configured as not-mapped (ignored)
public string Id_Name {
get {
if(_id_Name == null){
_id_Name = $"{Id}_{Name}";
}
return _id_Name;
}
set {
_id_Name = value;
}
}
string _id_Name;
//a method to parse back the Id & Name
public BarType ParseIdName(){
if(!string.IsNullOrWhiteSpace(Id_Name)){
var parts = Id_Name.Split(new[] {'_'}, 2);
Id = int.TryParse(parts[0], out var id) ? id : 0;
Name = parts.Length > 1 ? parts[1] : null;
}
return this;
}
}
Now instead of using Id for the selected value, you use Id_Name:
<select asp-for="BarType.Id_Name" asp-items="#(new SelectList(ViewBag.BarTypes, "Id_Name", "Name"))">
<option value="">please select</option>
</select>
Note that before actually using the bound BarType available from the model in the controller action, you need to manually call the method BarType.ParseIdNamelike this:
public override Task<IActionResult> Create(Bar entity)
{
entity.BarType?.ParseIdName();
//the entity is ready now ...
//...
return base.Create(entity);
}
The first option is simple but a bit tricky and I would not personally use it. The more standard way to do it is using the second option with a custom IModelBinder. This targets the model type BarType and resolves an Id into an instance of BarType. This resolution process should be fast.
public class BarTypeModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var fieldValue = bindingContext.ValueProvider.GetValue(bindingContext.FieldName).FirstValue;
//here we just instantiate an instance of BarType with Id but without Name
if(int.TryParse(fieldValue, out var id)){
bindingContext.Result = ModelBindingResult.Succeed(new BarType { Id = id });
}
else {
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
}
You should use that model binder like this:
[ModelBinder(typeof(BarTypeModelBinder))]
public class BarType : IdEntity
{
//...
}
That's almost done. Now talk about how you resolve instance of BarType from just its Id. As I said, usually we just need the Id so just create an instance of BarType containing just Id (as the code above does) is enough. That's of course very fast. If you need the Name as well, you may have to resolve the instance of BarType using some service. Because it's required to be fast, you really need some kind of in-memory lookup (or cached data). Suppose you have a service to resolve a BarType instance from its Id like this:
public interface IBarTypeService {
BarType GetBarType(int id);
}
You can use that service in the BarTypeModelBinder like this:
if(int.TryParse(fieldValue, out var id)){
var barType = _barTypeService.GetBarType(id);
bindingContext.Result = ModelBindingResult.Succeed(barType);
}
else {
bindingContext.Result = ModelBindingResult.Failed();
}
The second option may be a bit complicated for beginners (because it involves a nice IBarTypeService with support of caching or at least a nice way to feed it with some in-memory data lookup) but really it's the standard way to go. Once you become familiar with that logic, it just feels normal.
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);
}
My model has an property whcih I assigned a ReadOnly tag. My intention is to set a propery to readonly true or false depending of a condition like
class Test {
static bool test() { // It is my model
// My logical response ...
return true;
}
[ReadOnly(test)]
datetime prop {get; set;}
}
using this model I get the error message:
Error 7 An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter
Could you, pleaee, get me an idea for this?
=====================================================================================
Solution like answer 3:
Inside Template:
cshtml:
...
#if (Model.condition) {
<td>#Html.EditorFor(m => m.prop)</td>
} else {
<td>#Html.DisplayFor(m => m.prop)</td>
}
...
It will be inside the template.
Inside Model in the copmnstructor I set the condition of the property condition:
class XX {
public condition { get; set; } // not necessary readonly, I do the logical inside the template.
public datetime prop {get; set; }
public XX (bool _condition) {
condition = _condition;
}
}
You cannot use something other than described in the error message as the parameter for attributes.
It is a sad true, but still a true.
Only this:
[ReadOnly(5)]
[ReadOnly("string")] // Or other type (int/long/byte/etc..) which can be used with const keyword.
[ReadOnly(Enums.SomeValue)]
[ReadOnly(typeof(SomeType))]
[ReadOnly(new string[] { "array", "of", "strings"} )]
So unfortunately, you wont succeed making custom parameter type:
class ReadOnlyAttribute { ReadOnlyAttribute(MyClass foo) { ... } }
One alternative would be to do it within the get/set, something like:
class test
{
static bool test() {
...
}
private datetime prop;
public datetime Prop
{
get { return prop; }
set
{
if (test)
prop = value;
else
// Ignore, throw exception, etc.
}
}
}
The metadata for the model (which includes your IsReadOnly) is created by the Model Metadata providers. This providers only have information about data types, and property names, but not about the concrete values of the properties of an instance of the model. So the metadata can not depend on the value of a property or method of the model class. (So implementing a Custom ModelMetada Provider wouldn't solve your problem).
Then, you have to find an alternative, hacky, way to do it:
Create a view model with two properties, the original, without the readonly attribute and an additional readonly property, decorated with the readonly attribute.
In the view, decide which of the two to show.
public class MyModel
{
public DateTime MyProperty { get; set;}
[ReadOnly]
public DateTime MyPropertyRo { get; set;}
}
If you want to recover the posted values, the editable version should use the original property in the Telerik control. The non-editable version should use the readonly property in the Telerik control, and the original property in a hidden-field, so that you can recover it in the post.
#if (Model.MyPropertyIsReadOnly)
{
#Html.HiddenFor(m => m.Property)
#Html.TelerikEditorFor(m => m.PropertyRo ...)
}
else
{
#Html.TelerikEditorFor(m => m.Property ...)
}
If you have to do this in many different views, you can create an Html helper (extension method for Html), which receives the 3 properties and includes the last sample code.
Finally, it would be even better to make a custom Editor template, but that's much harder to do if you don't have experience.
There is still another option: contact telerik, and ask them to implement a version of their control which receives a readonly parameter, and does this automatically for you. I think it must be really easy for them to implement it. So, if you're lucky enough...
There is something weird going on in my app. It's not dammageable, but it's a curious behavior and I'm reaching out to you to understand what's happening.
I was working on some partial view based on a model, and it worked. I figured out I had to replace a lot of stuff with the correct input.
So here's a snippet of my old model:
public class SearchObjInfo
{
public string m_ObjName { get; set; }
public string m_ObjType { get; set; }
public decimal? m_ObjNumber { get; set; }
public string m_ObjSymbol { get; set; }
public string m_ObjPower { get; set; }
}
And here's the same snippet with the new class I made to construct this partial view:
public class SearchObjInfoPartial
{
public string m_ObjName { get; set; }
public IEnumerable<SelectListItem> m_ObjType { get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:0}")]
public int m_ObjNumber { get; set; }
public IEnumerable<SelectListItem> m_ObjPower { get; set; }
public IEnumerable<SelectListItem> m_ObjSymbol { get; set; }
}
Now the way the render is made is actually quite identical, and not. I used lots of stuff like these before:
<label>
Text: Write a name, part of a name, or a word.
</label>
Object Name: #Html.TextBox("_objectName") <br/>
Object Number: <input type="number" min="0" max="9999" name="_objNumber" value="decimal" style="width: 70px"/><br/>
Type: #Html.DropDownList("_objType", "All") <br/>
Power: #Html.DropDownList("_objSymbol", "=") #Html.DropDownList("_objValue", String.Empty)<br/>
But now I render my partial view this way:
#model MyApp.Utilities.SearchObjInfoPartial
Object Name: #Html.TextBoxFor(item => item.m_ObjName, Model.m_ObjName, String.Empty) <br/>
Object Number: #Html.EditorFor(item => item.m_ObjNumber)<br />
Power: #Html.DropDownListFor(item => item.m_ObjPower, Model.m_ObjPower, String.Empty) #Html.DropDownListFor(item => item.m_ObjSymbol, Model.m_ObjSymbol, String.Empty)
Type: #Html.DropDownListFor(item => item.m_ObjType, Model.m_ObjType, String.Empty) <br/>
Before rendering I deal with the SelectLists, no problems here.
Now here's where it gets interesting:
In my controllers I used to have methods receiving huge amounts of data (see here: How to deal with many possible values to make a query?)
But now I made something else. Without thinking, I tried to add the old search model in the controller method like this:
public ActionResult BrowseObjectList(SearchObjInfo searchObj, string _objName, (...))
And I just found out that it works even if the receiving object is not the same as the one used in my partial view model. How is that even possible? I mean, the proper fields will fill up and I can "safely" deal with my searchObj item, though I do not find this secure after all...
Thats what MVC framework does for you man.
Browser simply sends the form collection to server as Name Value Collection. As the request hits server, MVC framework will match the values with parameter in the Action method.
Form collection values are mapped to Model object properties. This is done by doing a match with Property Name and Name of the value in Form collection. Just check the client side code by view source, you can see that the input tags will have an attribute 'name' which matches with the property name of model.
QueryString values will also be mapped to parameters in Action method based on name.
Even you add a hidden field and specify a parameter with same name in action method...tada you will get the value of hidden field in that variable on post back
In your case though the model is different, its property name are same m_ObjName, m_ObjType, m_ObjNumber, m_ObjSymbol, m_ObjPower. So MVC do a match for you.
Try with different property name and see the results ;-)