So I have a model, other properties removed for brevity:
public class OutOfLimitReasonViewModel
{
[Required]
public int ReasonId { get; set; }
[Required(ErrorMessage = "Other Reason is required")]
public string OtherReason { get; set; }
}
Of course I have an EditForm on the .razor page and the InputText I care about looks like this:
<InputText #bind-Value="model.OtherReason" class="form-control" disabled="#(!OtherReasonRequired)" />
<ValidationMessage For="#(() => model.OtherReason)" />
There is also a select that has the list of available Reason objects, one of which is Other.
I do have a property called OtherReasonRequired which graphically does what I want (either Enable or Disable the input based on if the selected Reason == "Other") so that code works:
public bool OtherReasonRequired
{
get
{
var result = false;
if (model.ReasonId > 0)
{
var reason = Reasons.Find(x => x.Id == model.ReasonId);
result = reason.Reason == "Other";
}
return result;
}
}
This works perfectly if I select Other and give OtherReason a value, the Save/Submit button is valid and it works.
My issue is when I have NOT selected Other. Graphically, the InputField does get disabled and grayed out. But the Save/Submit believes the model is invalid because there is no value in the OtherReason field.
Is there something I can do to get this to work?
Would love to have a dynamic attribute called RequiredIf.
But logically to me, I see this as a bug. If a control is disabled, it's value is irrelevant in my mind.
Please check the following. I don't use any of the built-in Data Annotation, but it's still very easy to check the input and let the user know what I want them to do:
<EditForm Model="#DisplayModel" OnValidSubmit="HandleValidSubmit" >
<InputRadioGroup TValue=int? #bind-Value=DisplayModel.ReasonID>
#for (int i=1; i< Reasons.Count(); i++){
<InputRadio Value=Reasons[i].ID /> #Reasons[i].Description <br/>
}
<InputRadio Value=0 />Other <br/>
</InputRadioGroup>
<input #bind=DisplayModel.OtherText disabled=#((DisplayModel.ReasonID??1) !=0 ) />
<button type="submit">Submit</button>
<div class="text-danger">#DisplayMessage</div>
</EditForm>
#code {
string DisplayMessage="";
class Reason { public int? ID; public string? Description; }
List<Reason> Reasons = new List<Reason> {
new Reason { ID = 0, Description = "Other"},
new Reason { ID = 1, Description = "You're lame" },
new Reason { ID = 2, Description = "I'm too busy" }
};
class MyDisplayModel
{
public int? ReasonID;
public string OtherText="";
}
MyDisplayModel DisplayModel = new MyDisplayModel();
private async Task HandleValidSubmit()
{
if(DisplayModel.ReasonID is not null) if (DisplayModel.ReasonID==0) if (DisplayModel.OtherText == "")
{
DisplayMessage = "You must type a description of your issue.";
StateHasChanged();
await Task.Delay(1500);
DisplayMessage = "";
}
}
}
I've recently started using Blazor. Is there a way to trigger form model validation only on submit, instead of live on each change?
Just for clarification, let's say I have something like this:
<EditForm Model="this" OnValidSubmit="SubmitForm">
<DataAnnotationsValidator />
<ValidationSummary />
<Label For="Name">Name</Label>
<InputText id="Name" name="Name" class="form-control" #bind-Value="Name"/>
<button type="submit">Save</button>
</EditForm>
#code {
[StringLength(10, ErrorMessage="Name too long")]
public string Name { get; set; }
private async Task SubmitForm()
{
// ...
// send a POST request
}
}
By default, it seems like the validity of the field and the error messages displayed in the ValidationSummary get re-evaluated on every change of the text input (e.g. as soon as I delete the 11th character from the input, the "too long" message disappears).
I would prefer if the displayed messages would remain frozen until the Submit button is clicked.
I suppose it would be possible to implement it by removing the ValidationSummary component and implementing a custom solution (e.g. displaying a List of error messages that's refreshed only on submit), but I was wondering if there is some idiomatic solution that I'm not aware of.
When validation occurs is controlled by the Validator you're using.
There are two events that you can receive from EditContext:
OnValidationRequested is invoked either when EditContext.Validate is called or as part of the form submission process.
OnFieldChanged is invoked every time a field value is changed.
A validator uses these events to trigger it's validation process, and outputs the results to the EditContext's ValidationMessageStore.
DataAnnotationsValidator wires up for both events and triggers validation whenever either is invoked.
There are other validators out there, and writing your own is not too difficult. Other than those from the usual control suppliers, there's Blazored, or mine. Mine is documented here - https://shauncurtis.github.io/articles/Blazor-Form-Validation.html. it has a DoValidationOnFieldChange setting!
#enet's answer sparked an alternative answer. Build your own DataAnnotationsValidator.
Here's the EditContext Extensions code. It's a modified version of the original MS Code with some extra control arguments.
using Microsoft.AspNetCore.Components.Forms;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
namespace StackOverflowAnswers;
public static class EditContextCustomValidationExtensions
{
public static IDisposable EnableCustomValidation(this EditContext editContext, bool doFieldValidation, bool clearMessageStore)
=> new DataAnnotationsEventSubscriptions(editContext, doFieldValidation, clearMessageStore);
private static event Action? OnClearCache;
private static void ClearCache(Type[]? _)
=> OnClearCache?.Invoke();
private sealed class DataAnnotationsEventSubscriptions : IDisposable
{
private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache = new();
private readonly EditContext _editContext;
private readonly ValidationMessageStore _messages;
private bool _doFieldValidation;
private bool _clearMessageStore;
public DataAnnotationsEventSubscriptions(EditContext editContext, bool doFieldValidation, bool clearMessageStore)
{
_doFieldValidation = doFieldValidation;
_clearMessageStore = clearMessageStore;
_editContext = editContext ?? throw new ArgumentNullException(nameof(editContext));
_messages = new ValidationMessageStore(_editContext);
if (doFieldValidation)
_editContext.OnFieldChanged += OnFieldChanged;
_editContext.OnValidationRequested += OnValidationRequested;
if (MetadataUpdater.IsSupported)
{
OnClearCache += ClearCache;
}
}
private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs)
{
var fieldIdentifier = eventArgs.FieldIdentifier;
if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
{
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
var validationContext = new ValidationContext(fieldIdentifier.Model)
{
MemberName = propertyInfo.Name
};
var results = new List<ValidationResult>();
Validator.TryValidateProperty(propertyValue, validationContext, results);
_messages.Clear(fieldIdentifier);
foreach (var result in CollectionsMarshal.AsSpan(results))
{
_messages.Add(fieldIdentifier, result.ErrorMessage!);
}
// We have to notify even if there were no messages before and are still no messages now,
// because the "state" that changed might be the completion of some async validation task
_editContext.NotifyValidationStateChanged();
}
}
private void OnValidationRequested(object? sender, ValidationRequestedEventArgs e)
{
var validationContext = new ValidationContext(_editContext.Model);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(_editContext.Model, validationContext, validationResults, true);
// Transfer results to the ValidationMessageStore
_messages.Clear();
foreach (var validationResult in validationResults)
{
if (validationResult == null)
{
continue;
}
var hasMemberNames = false;
foreach (var memberName in validationResult.MemberNames)
{
hasMemberNames = true;
_messages.Add(_editContext.Field(memberName), validationResult.ErrorMessage!);
}
if (!hasMemberNames)
{
_messages.Add(new FieldIdentifier(_editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage!);
}
}
_editContext.NotifyValidationStateChanged();
}
public void Dispose()
{
if (_clearMessageStore)
_messages.Clear();
if (_doFieldValidation)
_editContext.OnFieldChanged -= OnFieldChanged;
_editContext.OnValidationRequested -= OnValidationRequested;
_editContext.NotifyValidationStateChanged();
if (MetadataUpdater.IsSupported)
{
OnClearCache -= ClearCache;
}
}
private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo? propertyInfo)
{
var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo))
{
// DataAnnotations only validates public properties, so that's all we'll look for
// If we can't find it, cache 'null' so we don't have to try again next time
propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
// No need to lock, because it doesn't matter if we write the same value twice
_propertyInfoCache[cacheKey] = propertyInfo;
}
return propertyInfo != null;
}
internal void ClearCache()
=> _propertyInfoCache.Clear();
}
}
And the CustomValidation component:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
namespace StackOverflowAnswers;
public class CustomValidation : ComponentBase, IDisposable
{
private IDisposable? _subscriptions;
private EditContext? _originalEditContext;
[CascadingParameter] EditContext? CurrentEditContext { get; set; }
[Parameter] public bool DoEditValidation { get; set; } = false;
/// <inheritdoc />
protected override void OnInitialized()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)} " +
$"inside an EditForm.");
}
_subscriptions = CurrentEditContext.EnableCustomValidation(DoEditValidation, true);
_originalEditContext = CurrentEditContext;
}
/// <inheritdoc />
protected override void OnParametersSet()
{
if (CurrentEditContext != _originalEditContext)
{
// While we could support this, there's no known use case presently. Since InputBase doesn't support it,
// it's more understandable to have the same restriction.
throw new InvalidOperationException($"{GetType()} does not support changing the " +
$"{nameof(EditContext)} dynamically.");
}
}
/// <inheritdoc/>
protected virtual void Dispose(bool disposing)
{
}
void IDisposable.Dispose()
{
_subscriptions?.Dispose();
_subscriptions = null;
Dispose(disposing: true);
}
}
You can use it like this:
<EditForm EditContext=this.editContext OnValidSubmit=OnValidSubmit>
<CustomValidation DoEditValidation=false/>
#*<DataAnnotationsValidator/>*#
<div class="row">
<div class="col-2">
Date:
</div>
<div class="col-10">
<InputDate #bind-Value=this.Record.Date></InputDate>
</div>
</div>
.......
I have something like this in mij Index.razor file:
<div class="trials">
#DoSomething("First try")
#(async () => await DoSomething("Second try"))
#DoSomething("Third try").Result
</div>
<div class="purpose">
<h4>#DoSomething("Use the result like a title")</h4>
<textarea placeholder="#DoSomething("Use the result like a placeholder")" />
<button type="button" class="btn">#DoSomething("Show the result on a button")</button>
</div>
#code {
private async Task<string> DoSomething(string text)
{
return await client.DoSomething(text);
}
}
I want to show the string result of the DoSomething() on headers, buttons, placeholders etc. But I cannot get it to work. I have tried different solutions.
First try:
#DoSomething("First try")
Returns System.Threading.Tasks.Task`1[System.String] instead of the result I expect.
Second try:
#(async () => await DoSomething("Second try"))
It says this is not valid because: Cannot convert lambda expression to type 'object' because it is not a delegate type
Third try:
#DoSomething("Third try").Result
Does not come back, the application will freeze.
It is possible to store the result in a variable or property, but this is not a solution for me because I will use it like everywhere, on buttons, placeholders and stuff.
How do I show the result of DoSomething() on a header/button/placeholder/etc?
The 'cleanest' I can think of is a Component for the job:
// AsyncHelper.razor
#result
#code {
[Parameter]
public Func<string, Task<string>> Formatter { get; set; }
[Parameter]
public string Text { get; set; }
private string result;
protected override async Task OnParametersSetAsync()
{
result = await Source(Text);
}
}
and then use it like
<h4><AsyncHelper Formatter="DoSomething" Text ="Use the result like a title" /></h4>
The correct way to do this is as follows
<div class="trials">
#Value1
#Value2
#Value3
</div>
#code
{
protected override async Task OnInitializedAsync()
{
Value1 = "1";
Value2 = await DoSomethingAsync().ConfigureAwait(false);
Value3 = "3";
}
}
I have a simple test application which reproduces an error I encountered recently. Basically I have a simple WinForm with databound TextBox and DateTimePicker controls, and a button. When I execute the code below (on the button click), I get the error "DataBinding cannot find a row in the list that is suitable for all bindings". If I move the DataSource assignment into the form's constructor, I don't get the error.
If I remove the data binding for the DateTimePicker, it works fine.
Can anyone explain what the problem is ?
public partial class Form1 : Form
{
private BindingSource bs;
public Form1()
{
InitializeComponent();
button1.Click += new EventHandler(button1_Click);
bs = new BindingSource();
bs.DataSource = typeof(Thing);
this.textBox1.DataBindings.Add("Text", bs, "MyString");
this.dateTimePicker1.DataBindings.Add(new Binding("Value", bs, "MyDate"));
//Thing thing = new Thing { MyString = "Hello", MyNumber = 123, MyDate = DateTime.Parse("01-Jan-1970") };
//bs.DataSource = thing;
}
private void button1_Click(object sender, EventArgs e)
{
Thing thing = new Thing { MyString = "Hello", MyNumber = 123, MyDate = DateTime.Parse("01-Jan-1970") };
bs.DataSource = thing;
}
}
public partial class Thing
{
public String MyString { get; set; }
public Int32 MyNumber { get; set; }
public DateTime MyDate { get; set; }
}
}
Thanks
Edit:
It seems that if I change the data binding for the DateTimePicker control such that I bind to the "Text" property, the problem goes away. I don't understand why that would be though, because "Value" is valid for data binding.
I have been trying to add some events to the fullCalendar using a call to a ASHX page using the following code.
Page script:
<script type="text/javascript">
$(document).ready(function() {
$('#calendar').fullCalendar({
header: {
left: 'prev,next today', center: 'title', right: 'month, agendaWeek,agendaDay'
},
events: 'FullCalendarEvents.ashx'
})
});
</script>
c# code:
public class EventsData
{
public int id { get; set; }
public string title { get; set; }
public string start { get; set; }
public string end { get; set; }
public string url { get; set; }
public int accountId { get; set; }
}
public class FullCalendarEvents : IHttpHandler
{
private static List<EventsData> testEventsData = new List<EventsData>
{
new EventsData {accountId = 0, title = "test 1", start = DateTime.Now.ToString("yyyy-MM-dd"), id=0},
new EventsData{ accountId = 1, title="test 2", start = DateTime.Now.AddHours(2).ToString("yyyy-MM-dd"), id=2}
};
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "application/json.";
context.Response.Write(GetEventData());
}
private string GetEventData()
{
List<EventsData> ed = testEventsData;
StringBuilder sb = new StringBuilder();
sb.Append("[");
foreach (var data in ed)
{
sb.Append("{");
sb.Append(string.Format("id: {0},", data.id));
sb.Append(string.Format("title:'{0}',", data.title));
sb.Append(string.Format("start: '{0}',", data.start));
sb.Append("allDay: false");
sb.Append("},");
}
sb.Remove(sb.Length - 1, 1);
sb.Append("]");
return sb.ToString();
}
}
The ASHX page gets called and returnd the following data:
[{id: 0,title:'test 1',start: '2010-06-07',allDay: false},{id: 2,title:'test 2',start: '2010-06-07',allDay: false}]
The call to the ASHX page does not display any results, but if I paste the values returned directly into the events it displays correctly. I am I have been trying to get this code to work for a day now and I can't see why the events are not getting set.
Any help or advise on how I can get this to work would be appreciated.
Steve
In case anyone stumbles across this problem. I tried all of the above solutions, but none of them worked.
For me, the problem was solved by using an older version of jquery. I switched from version 1.5.2 which was included in the fullcalendar package to version 1.3.2
Steve,
I ran into something similar -- it would render the events if the JSON was directly in the fullCalendar call, but it would not render the identicla JSON coming from an outside URL. I finally got it to work by modifying the JSON so that "id", "title", "start", "end", and "allDay" had the quotes around them.
So instead of this (to use your sample JSON):
[{id: 0,title:'test 1',start: '2010-06-07',allDay: false},{id: 2,title:'test 2',start: '2010-06-07',allDay: false}]
...I had this:
[{"id": 0,"title":"test 1","start": "2010-06-07","allDay": false},{"id": 2,"title":"test 2","start": "2010-06-07","allDay": false}]
Now, why it worked locally but not remotely, I can't say.
Your JSON data lost the end item:
{id: 0,title:'test 1',start: '2010-06-07',end: '2010-06-07',allDay: false}
Let's look at what we know and eliminate possibilities:
The ASHX page gets called and returnd the ... data:
So the server-side portion is working just fine, and the code to call out to the server side is working.
I paste the values returned directly into the events it displays correctly.
So the code that handles the response works correctly.
Logically we see here that code that connects your server response to your calendar's input is not working. Unfortunately, I'm not up on the jQuery fullCalendar method, but perhaps you're missing a callback declaration?
I think it might have something to do with your date values.
FullCalendar events from asp.net ASHX page not displaying is a correct solution to the issue.
And I used long format for dates.
And #Steve instead of StringAppending we can use :-
System.Web.Script.Serialization.JavaScriptSerializer oSerializer =
new System.Web.Script.Serialization.JavaScriptSerializer();
String sJSON = oSerializer.Serialize(evList);
evList being your list containing all events which has the essential properties like id,start,end,description,allDay etc..
I know this thread is an old thread,but this will be helpful to other users.
Just collating all the answers.
I struggled with this issue and resolved it using an .ashx handler as follows
My return class looks like…
public class Event
{
public Guid id { get; set; }
public string title { get; set; }
public string description { get; set; }
public long start { get; set; }
public long end { get; set; }
public bool allDay { get; set; }
}
Where DateTime values are converted to long values using…
private long ConvertToTimestamp(DateTime value)
{
long epoch = (value.ToUniversalTime().Ticks - 621355968000000000) / 10000000;
return epoch;
}
And the ProcessRequest looks like…
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/html";
DateTime start = new DateTime(1970, 1, 1);
DateTime end = new DateTime(1970, 1, 1);
try
{
start = start.AddSeconds(double.Parse(context.Request.QueryString["start"]));
end = end.AddSeconds(double.Parse(context.Request.QueryString["end"]));
}
catch
{
start = DateTime.Today;
end = DateTime.Today.AddDays(1);
}
List<Event> evList = new List<Event>();
using (CondoManagerLib.Linq.CondoDataContext Dc = new CondoManagerLib.Linq.CondoDataContext(AppCode.Common.CGlobals.DsnDB))
{
evList = (from P in Dc.DataDailySchedules
where P.DateBeg>=start && P.DateEnd<=end
select new Event
{ description = P.Description,
id = P.RecordGuid,
title = P.Reason,
start = ConvertToTimestamp(P.DateBeg),
end = ConvertToTimestamp(P.DateEnd),
allDay = IsAllDay(P.DateBeg, P.DateEnd)
}).ToList();
}
System.Web.Script.Serialization.JavaScriptSerializer oSerializer = new System.Web.Script.Serialization.JavaScriptSerializer();
String sJSON = oSerializer.Serialize(evList);
context.Response.Write(sJSON);
}
And my Document Ready…
> $(document).ready(function () {
> $('#calendar').fullCalendar({
> header: { left: 'title', center: 'prev,today,next', right: 'month,agendaWeek,agendaDay' },
> editable: false,
> aspectRatio: 2.1,
> events: "CalendarEvents.ashx",
> eventRender: function (event, element) {
> element.qtip({
> content: event.description,
> position: { corner: { tooltip: 'topLeft', target: 'centerLeft'} },
> style: { border: { width: 1, radius: 3, color: '#000'},
> padding: 5,
> textAlign: 'center',
> tip: true,
> name: 'cream'
> }
> });
> }
> })
> });
The qTip pluging can be found at http://craigsworks.com/projects/qtip/
Hope this helps.