MudBlazor icon indicating copy to clipboard operation
MudBlazor copied to clipboard

DateOnly support for calendars

Open sjd2021 opened this issue 2 years ago • 12 comments

Feature request type

Enhance component

Component name

Date Picker

Is your feature request related to a problem?

We love Mud Blazor, but many of us .NET 6/7 users like to use DateOnly for our models that are binded to our form fields. The date pickers do not currently work with DateOnly even when we've specified that we are not interested in time. As a result, we have to write things in automapper which map to our database models. However, doing so seems to make the relationship too difficult for AutoMapper/EF to translate any queries that were written with Linq + ProjectTo. It's causing us to not be able to use the right tools for the right job and accurately reflect our db data types in our domain models.

Describe the solution you'd like

We'd like to be able to use DateOnly for calendars that simply need a date such as a birthday or closing date.

Have you seen this feature anywhere else?

Only in custom implementations

Describe alternatives you've considered

Automapper from a temporary model used only to support mudblazor - the issue with this is that we use ProjectTo to convert queries from our models to our domain db entities, and the mapping becomes too complex to be translated by EF.

Pull Request

  • [ ] I would like to do a Pull Request

Code of Conduct

  • [X] I agree to follow this project's Code of Conduct

sjd2021 avatar Jan 18 '23 04:01 sjd2021

For anyone who needs to get DateOnly working I made a small adapter to use in my project and thought I'd share:

<MudDatePicker Label="@Label" Class="d-inline-flex" Editable @bind-Date="_date" For="@(() => _date)" />

@*HACK: This component works as an adapter between MudDatePicker which only accepts DateTime? and any DateOnly object.
    Remove once issue https://github.com/MudBlazor/MudBlazor/issues/6178 is fixed*@
    
@code {
    [Parameter]
    [EditorRequired]
    public DateOnly Date { get; set; }
    
    [Parameter]
    public EventCallback<DateOnly> DateChanged { get; set; }

    [Parameter]
    [EditorRequired]
    public string? Label { get; set; }
    
    private DateTime? _date
    {
        get => Date.ToDateTime(TimeOnly.MinValue);
        set
        {
            if (value is not null)
            {
                Date = DateOnly.FromDateTime((DateTime)value);
                DateChanged.InvokeAsync(Date);
            }
        }
    }
}

Luk164 avatar Jan 27 '23 07:01 Luk164

@Luk164 Have you found a way to support the validation parameter "For"? I have done the same as you to support DateOnly., but converting this expression and still getting the correct parameter path does not seem possible.

archigo avatar Dec 06 '23 11:12 archigo

@Luk164 Have you found a way to support the validation parameter "For"? I have done the same as you to support DateOnly., but converting this expression and still getting the correct parameter path does not seem possible.

The adapter fully wraps the DateTime picker, so if you want to validate the easiest way would be to do so in the adaptor itself

Luk164 avatar Dec 06 '23 11:12 Luk164

the For parameter is used to determine where to show a validation message, so it is not a question of just validating. Mudblazor uses it this way:

MudFormComponent.cs

            var expression = (MemberExpression)For.Body;
            var propertyInfo = expression.Expression?.Type.GetProperty(expression.Member.Name);
            _validationAttrsFor = propertyInfo?.GetCustomAttributes(typeof(ValidationAttribute), true).Cast<ValidationAttribute>();

            _fieldIdentifier = FieldIdentifier.Create(For);
            _currentFor = For;

archigo avatar Dec 06 '23 11:12 archigo

@archigo Sorry it has been almost a year since I made that, I will have to look deeper once I have some more time

Luk164 avatar Dec 06 '23 12:12 Luk164

No problem, i was just curios, since i did not find a way to solve it myself.

Instead our DateOnlyPicker will have to use the Validation parameter, instead of using the model validation that we otherwise use.

archigo avatar Dec 06 '23 12:12 archigo

No problem, i was just curios, since i did not find a way to solve it myself.

Instead our DateOnlyPicker will have to use the Validation parameter, instead of using the model validation that we otherwise use.

I do believe you can just invoke model validation manually using ValidationContext

https://stackoverflow.com/questions/17138749/how-to-manually-validate-a-model-with-attributes

Luk164 avatar Dec 06 '23 12:12 Luk164

I think we are talking past each other. The issue is not invoking validation, but telling the MudComponent that a validation error belongs to it. That is what the "For" parameter does.

archigo avatar Dec 07 '23 08:12 archigo

@archigo I managed to work around having to use the For parameter with reflection. (Which isn't that nice, I know, but it works.)

So for an explanation: The only thing For does, that's important for our use case (showing the appropriate validation message for the input), seems to be this line from the code you already posted above:

MudFormComponent.cs

            ...
            _fieldIdentifier = FieldIdentifier.Create(For);
            ...

So instead of setting the For parameter, which we can't do as it expects a DateTime? instead of a DateOnly?, we can set the _fieldIdentifier directly using reflection like this:

var fieldIdentifierField = typeof(MudFormComponent<DateTime?, string>).GetField("_fieldIdentifier", BindingFlags.Instance | BindingFlags.NonPublic);
if (fieldIdentifierField != null)
{
    fieldIdentifierField.SetValue(datePicker, editContext.Field("NameOfYourDateOnlyField"));
}

(datePicker is just a ref to the MudDatePicker. So this has to be done in OnAfterRender() for the ref to be set.)

Hope this helps!

ar-schraml avatar Feb 20 '24 15:02 ar-schraml

@ar-schraml Thanks a lot! Your solution worked great. I only had one issue, which is when there are multiple levels to the form

Model<FormLevel1>:

class FormLevel1 {
    public DateOnly DateWorkByString;
    public FormLevel2 SubModel;
}

class FormLevel2 {
    public DateOnly DateDoesNitWorkByString;
}

I could not get the the field identifier to work by setting it based on a string. I think this is because the object reference of the field identifier is wrong.

I solved this by doing the same thing the MudBlazor does, which also has the advantage of mainting the for parameter in the same way that we are used to from normal MudBlazor components:

[CascadingParameter]
public EditContext? EditContext { get; set; }

[Parameter]
public Expression<Func<DateOnly?>>? For { get; set; }

private MudDatePicker? _mudDatePicker;

protected override void OnAfterRender(bool firstRender)
{
    if (firstRender && For != null)
    {
        if (EditContext == null)
            throw new Exception("Using 'For' without an 'EditContext' is not supported. Are you missing an 'EditForm'?");

        // get the private fieldidentifier by reflection.
        var fieldIdentifierField = typeof(MudFormComponent<DateTime?, string>).GetField("_fieldIdentifier", BindingFlags.Instance | BindingFlags.NonPublic);
        if (fieldIdentifierField != null)
        {
            // set the field identifier with our DateOnly expression, avoiding the type issue between DateOnly vs DateTime
            fieldIdentifierField.SetValue(_mudDatePicker, FieldIdentifier.Create(For));
        }
    }
}

archigo avatar Apr 11 '24 13:04 archigo

Thanks to your help I finally could implement my DateOnlyPicker.

Just for convenience because we all like copy and pasta here the full component.

@using System.Linq.Expressions
@using System.Reflection

<MudDatePicker @ref="datePickerRef" Label="@Label" Class="d-inline-flex" ReadOnly="@ReadOnly" HelperText="@HelperText" @bind-Date="dateBindTarget" />

@*HACK: This component works as an adapter between MudDatePicker which only accepts DateTime? and any DateOnly? object.
Remove once issue https://github.com/MudBlazor/MudBlazor/issues/6178 is fixed*@

@code
{
	[CascadingParameter]
	public EditContext? EditContext { get; set; }

	[Parameter, EditorRequired]
	public DateOnly? Date { get; set; }

	[Parameter]
	public EventCallback<DateOnly?> DateChanged { get; set; }

	[Parameter, EditorRequired]
	public string? Label { get; set; }

	[Parameter]
	public Expression<Func<DateOnly?>>? For { get; set; }

	[Parameter]
	public bool ReadOnly { get; set; }

	[Parameter]
	public string? HelperText { get; set; }

	private MudDatePicker? datePickerRef;

	private DateTime? dateBindTarget
	{
		get => Date?.ToDateTime(TimeOnly.MinValue);
		set
		{
			if (value is not null)
			{
				Date = DateOnly.FromDateTime((DateTime)value);
				DateChanged.InvokeAsync(Date);
			}
		}
	}

	protected override void OnAfterRender(bool firstRender)
	{
		if (!firstRender || For is null)
			return;

		if (EditContext is null)
			throw new Exception("Using 'For' without an 'EditContext' is not supported. Are you missing an 'EditForm'?");

		// Get the private field _fieldidentifier by reflection.
		FieldInfo? fieldIdentifierField = typeof(MudFormComponent<DateTime?, string>).GetField("_fieldIdentifier", BindingFlags.Instance | BindingFlags.NonPublic);

		if (fieldIdentifierField is not null)
		{
			// Set the field identifier with our DateOnly? expression, avoiding the type issue between DateOnly vs DateTime
			fieldIdentifierField.SetValue(datePickerRef, FieldIdentifier.Create(For));
		}
	}
}

FelixCCWork avatar Apr 15 '24 10:04 FelixCCWork

So how does the For work when the parent is a MudForm? To be more specific, I'm getting the following error when I've got the DateOnlyPicker inside a MudForm:

System.AggregateException: One or more errors occurred. (Using 'For' without an 'EditContext' is not supported. Are you missing an 'EditForm'?)
       ---> System.Exception: Using 'For' without an 'EditContext' is not supported. Are you missing an 'EditForm'?

crosscuttech avatar May 19 '24 00:05 crosscuttech