Skip to content

Commit cf7c48c

Browse files
Lots of small improvements
1 parent 3a2abf6 commit cf7c48c

File tree

13 files changed

+441
-5
lines changed

13 files changed

+441
-5
lines changed

cleanup.bat

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
@echo off
2+
setlocal
3+
4+
rem Delete .vs directories
5+
for /d /r %%i in (.vs) do (
6+
if exist "%%i" (
7+
echo Deleting "%%i"
8+
rmdir /s /q "%%i"
9+
)
10+
)
11+
12+
rem Delete obj directories
13+
for /d /r %%i in (obj) do (
14+
if exist "%%i" (
15+
echo Deleting "%%i"
16+
rmdir /s /q "%%i"
17+
)
18+
)
19+
20+
rem Delete bin directories
21+
for /d /r %%i in (bin) do (
22+
if exist "%%i" (
23+
echo Deleting "%%i"
24+
rmdir /s /q "%%i"
25+
)
26+
)
27+
28+
echo Temporary files and directories have been removed.
29+
endlocal
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
@using DotNetElements.AppFramework.MudBlazorExtensions.Util
2+
3+
@typeparam TValue
4+
@typeparam TReturnValue
5+
6+
<MudDialog>
7+
<DialogContent>
8+
@if (Value is not null && editContext is not null)
9+
{
10+
<EditForm EditContext="@editContext">
11+
<DataAnnotationsValidator />
12+
@FormContent(Value)
13+
</EditForm>
14+
}
15+
else
16+
{
17+
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
18+
}
19+
</DialogContent>
20+
<DialogActions>
21+
<MudButton OnClick="OnCancelAsync_Internal">@CancelButtonText</MudButton>
22+
@if (Value is not null && editContext is not null)
23+
{
24+
<MudButton Color="Color.Primary" OnClick="OnSubmitAsync_Internal">@SubmitButtonText</MudButton>
25+
}
26+
</DialogActions>
27+
</MudDialog>
28+
29+
@code
30+
{
31+
[CascadingParameter]
32+
IMudDialogInstance DialogInstance { get; set; } = default!;
33+
34+
[Parameter, EditorRequired]
35+
public TValue? Value { get; set; }
36+
37+
[Parameter, EditorRequired]
38+
public RenderFragment<TValue> FormContent { get; set; } = default!;
39+
40+
[Parameter]
41+
public EventCallback<EditFormDialogArgs<TValue, TReturnValue>> OnSubmit { get; set; }
42+
43+
[Parameter]
44+
public EventCallback OnCancel { get; set; }
45+
46+
[Parameter]
47+
public string SubmitButtonText { get; set; } = "Submit";
48+
49+
[Parameter]
50+
public string CancelButtonText { get; set; } = "Cancel";
51+
52+
private EditContext? editContext;
53+
54+
protected override void OnParametersSet()
55+
{
56+
if (Value is null)
57+
return;
58+
59+
editContext = new EditContext(Value);
60+
}
61+
62+
private async Task OnSubmitAsync_Internal()
63+
{
64+
ArgumentNullException.ThrowIfNull(Value);
65+
ArgumentNullException.ThrowIfNull(editContext);
66+
67+
EditFormDialogArgs<TValue, TReturnValue> args = new(Value, editContext);
68+
69+
if (OnSubmit.HasDelegate)
70+
await OnSubmit.InvokeAsync(args);
71+
72+
if (args.Cancel)
73+
return;
74+
75+
DialogInstance.Close(DialogResult.Ok(args.ReturnValue));
76+
}
77+
78+
private async Task OnCancelAsync_Internal()
79+
{
80+
if (OnCancel.HasDelegate)
81+
await OnCancel.InvokeAsync();
82+
83+
DialogInstance.Cancel();
84+
}
85+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
@inherits MudComponentBase
2+
3+
@if (SimpleTable)
4+
{
5+
<th class="@Class" style="@($"width: {Width}px;") @Style">
6+
Actions
7+
</th>
8+
}
9+
else
10+
{
11+
<MudTh Class="@Class" Style="@($"width: {Width}px; {@Style}")">
12+
Actions
13+
</MudTh>
14+
}
15+
16+
@code
17+
{
18+
[Parameter]
19+
public bool SimpleTable { get; set; }
20+
21+
[Parameter]
22+
public int Width { get; set; } = 140;
23+
}

src/DotNetElements.AppFramework.MudBlazorExtensions/DotNetElements.AppFramework.MudBlazorExtensions.csproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,4 @@
1919
<ProjectReference Include="..\DotNetElements.AppFramework.Abstractions\DotNetElements.AppFramework.Abstractions.csproj" />
2020
</ItemGroup>
2121

22-
<ItemGroup>
23-
<Folder Include="Util\" />
24-
</ItemGroup>
25-
2622
</Project>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Microsoft.AspNetCore.Components.Forms;
2+
3+
namespace DotNetElements.AppFramework.MudBlazorExtensions.Util;
4+
5+
public sealed record EditFormDialogArgs<T, TReturnValue>(T Value, EditContext EditContext)
6+
{
7+
public bool Cancel { get; set; }
8+
public TReturnValue? ReturnValue { get; set; }
9+
10+
public bool IsValid => EditContext.Validate();
11+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using Microsoft.AspNetCore.Components.Forms;
3+
4+
namespace DotNetElements.AppFramework.MudBlazorExtensions.Util;
5+
6+
public sealed class ModelValidationWrapper<TModel>
7+
{
8+
public required TModel Model { get; init; }
9+
public EditContext EditContext { get; private init; }
10+
11+
public bool IsValid { get; private set; } = true;
12+
public bool IsModified { get; private set; } = false;
13+
14+
[SetsRequiredMembers]
15+
public ModelValidationWrapper(TModel model)
16+
{
17+
ArgumentNullException.ThrowIfNull(model);
18+
19+
Model = model;
20+
EditContext = new EditContext(model);
21+
}
22+
23+
public bool Validate()
24+
{
25+
IsValid = EditContext.Validate();
26+
27+
return IsValid;
28+
}
29+
30+
public bool UpdateIsModified()
31+
{
32+
IsModified = EditContext.IsModified();
33+
34+
return IsModified;
35+
}
36+
}

src/DotNetElements.AppFramework.MudBlazorExtensions/_Imports.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@using Microsoft.AspNetCore.Components.Web
2+
@using Microsoft.AspNetCore.Components.Forms
23

34
@using global::MudBlazor
45

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System.Reflection;
2+
3+
namespace DotNetElements.Core.Validation;
4+
5+
public enum DateComparisonType
6+
{
7+
Equal,
8+
NotEqual,
9+
GreaterThan,
10+
GreaterThanOrEqual,
11+
LessThan,
12+
LessThanOrEqual
13+
}
14+
15+
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
16+
public sealed class CompareToDateAttribute : ValidationAttribute
17+
{
18+
public string OtherProperty { get; private init; }
19+
public DateComparisonType ComparisonType { get; private init; }
20+
21+
public CompareToDateAttribute(string otherProperty, DateComparisonType comparisonType)
22+
{
23+
OtherProperty = otherProperty;
24+
ComparisonType = comparisonType;
25+
}
26+
27+
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
28+
{
29+
PropertyInfo otherPropertyInfo = validationContext.ObjectType.GetProperty(OtherProperty, BindingFlags.Public | BindingFlags.Instance)
30+
?? throw new Exception($"Unknown property: {OtherProperty}");
31+
32+
object? otherValue = otherPropertyInfo.GetValue(validationContext.ObjectInstance);
33+
34+
// Nulls are considered valid (use [Required] for null checks)
35+
if (value is null || otherValue is null)
36+
return ValidationResult.Success;
37+
38+
// Normalize both values to DateTime for comparison
39+
DateTime? thisDate = value switch
40+
{
41+
DateTime dt => dt,
42+
DateTimeOffset dto => dto.UtcDateTime,
43+
_ => null
44+
};
45+
46+
DateTime? otherDate = otherValue switch
47+
{
48+
DateTime dt => dt,
49+
DateTimeOffset dto => dto.UtcDateTime,
50+
_ => null
51+
};
52+
53+
if (thisDate is null || otherDate is null)
54+
throw new Exception($"The {nameof(CompareToDateAttribute)} can only be applied to properties of type {nameof(DateTime)} or {nameof(DateTimeOffset)}.");
55+
56+
int comparison = DateTime.Compare(thisDate.Value, otherDate.Value);
57+
58+
bool isValid = ComparisonType switch
59+
{
60+
DateComparisonType.Equal => comparison == 0,
61+
DateComparisonType.NotEqual => comparison != 0,
62+
DateComparisonType.GreaterThan => comparison > 0,
63+
DateComparisonType.GreaterThanOrEqual => comparison >= 0,
64+
DateComparisonType.LessThan => comparison < 0,
65+
DateComparisonType.LessThanOrEqual => comparison <= 0,
66+
_ => throw new NotImplementedException(nameof(ComparisonType))
67+
};
68+
69+
if (isValid)
70+
return ValidationResult.Success;
71+
72+
return new ValidationResult(
73+
FormatErrorMessage(validationContext.DisplayName),
74+
validationContext.MemberName is not null ? [validationContext.MemberName] : null);
75+
}
76+
77+
public override string FormatErrorMessage(string name) => ErrorMessage ?? $"{name} must be {ComparisonType} {OtherProperty}.";
78+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.Reflection;
2+
3+
namespace DotNetElements.Core.Validation;
4+
5+
public enum NumericComparisonType
6+
{
7+
Equal,
8+
NotEqual,
9+
GreaterThan,
10+
GreaterThanOrEqual,
11+
LessThan,
12+
LessThanOrEqual
13+
}
14+
15+
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
16+
public sealed class CompareToNumericAttribute : ValidationAttribute
17+
{
18+
public string OtherProperty { get; }
19+
public NumericComparisonType ComparisonType { get; }
20+
21+
public CompareToNumericAttribute(string otherProperty, NumericComparisonType comparisonType)
22+
{
23+
OtherProperty = otherProperty;
24+
ComparisonType = comparisonType;
25+
}
26+
27+
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
28+
{
29+
PropertyInfo otherPropertyInfo = validationContext.ObjectType.GetProperty(OtherProperty, BindingFlags.Public | BindingFlags.Instance)
30+
?? throw new Exception($"Unknown property: {OtherProperty}");
31+
32+
object? otherValue = otherPropertyInfo.GetValue(validationContext.ObjectInstance);
33+
34+
// Nulls are considered valid (use [Required] for null checks)
35+
if (value is null || otherValue is null)
36+
return ValidationResult.Success;
37+
38+
// Ensure both values are of the same type or compatible types
39+
if (value.GetType() != otherValue.GetType())
40+
throw new Exception("Compared properties must be of the same type.");
41+
42+
if (value is not IComparable valueComp || otherValue is not IComparable otherComp)
43+
throw new Exception($"The {nameof(CompareToNumericAttribute)} can only be applied to properties that implement {nameof(IComparable)}.");
44+
45+
int comparison = valueComp.CompareTo(otherComp);
46+
47+
bool isValid = ComparisonType switch
48+
{
49+
NumericComparisonType.Equal => comparison == 0,
50+
NumericComparisonType.NotEqual => comparison != 0,
51+
NumericComparisonType.GreaterThan => comparison > 0,
52+
NumericComparisonType.GreaterThanOrEqual => comparison >= 0,
53+
NumericComparisonType.LessThan => comparison < 0,
54+
NumericComparisonType.LessThanOrEqual => comparison <= 0,
55+
_ => throw new NotImplementedException(nameof(ComparisonType))
56+
};
57+
58+
if (isValid)
59+
return ValidationResult.Success;
60+
61+
return new ValidationResult(
62+
FormatErrorMessage(validationContext.DisplayName),
63+
validationContext.MemberName is not null ? [validationContext.MemberName] : null);
64+
}
65+
66+
public override string FormatErrorMessage(string name) => ErrorMessage ?? $"{name} must be {ComparisonType} {OtherProperty}.";
67+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System.Globalization;
2+
3+
namespace DotNetElements.Core.Validation;
4+
5+
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
6+
public sealed class DateRangeAttribute : ValidationAttribute
7+
{
8+
public DateTime? Minimum { get; }
9+
public DateTime? Maximum { get; }
10+
11+
public DateRangeAttribute(string? minimum = null, string? maximum = null, string format = "yyyy-MM-dd")
12+
{
13+
if (!string.IsNullOrWhiteSpace(minimum))
14+
Minimum = DateTime.ParseExact(minimum, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
15+
16+
if (!string.IsNullOrWhiteSpace(maximum))
17+
Maximum = DateTime.ParseExact(maximum, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
18+
}
19+
20+
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
21+
{
22+
if (value is null)
23+
return ValidationResult.Success;
24+
25+
DateTime date;
26+
27+
if (value is DateTime dt)
28+
date = dt;
29+
else if (value is DateTimeOffset dto)
30+
date = dto.UtcDateTime;
31+
else
32+
throw new Exception($"The {nameof(DateRangeAttribute)} can only be applied to properties of type {nameof(DateTime)} or {nameof(DateTimeOffset)}.");
33+
34+
if (Minimum is not null && date < Minimum.Value)
35+
return new ValidationResult(
36+
ErrorMessage ?? $"Date must be on or after {Minimum.Value:yyyy-MM-dd}."
37+
, validationContext.MemberName is not null ? [validationContext.MemberName] : null);
38+
39+
if (Maximum is not null && date > Maximum.Value)
40+
return new ValidationResult(
41+
ErrorMessage ?? $"Date must be on or before {Maximum.Value:yyyy-MM-dd}."
42+
, validationContext.MemberName is not null ? [validationContext.MemberName] : null);
43+
44+
return ValidationResult.Success;
45+
}
46+
}

0 commit comments

Comments
 (0)