Add validation

This commit is contained in:
TheXamlGuy
2024-07-07 20:09:03 +01:00
parent 015af41fc8
commit 75bdd275c9
16 changed files with 142 additions and 104 deletions
+8
View File
@@ -4,6 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="using:Wallet" xmlns:vm="using:Wallet"
Margin="0,0,0,-18"
x:DataType="vm:ItemHeaderViewModel"> x:DataType="vm:ItemHeaderViewModel">
<UserControl.Resources> <UserControl.Resources>
<x:Double x:Key="PersonPictureSize">144</x:Double> <x:Double x:Key="PersonPictureSize">144</x:Double>
@@ -72,6 +73,7 @@
Text="&#xE36A;" /> Text="&#xE36A;" />
</DropDownButton> </DropDownButton>
</Grid> </Grid>
<StackPanel>
<TextBox <TextBox
MaxWidth="{StaticResource TextBoxMaxWidth}" MaxWidth="{StaticResource TextBoxMaxWidth}"
Text="{Binding Value}" Text="{Binding Value}"
@@ -130,5 +132,11 @@
</DataTriggerBehavior> </DataTriggerBehavior>
</Interaction.Behaviors> </Interaction.Behaviors>
</TextBox> </TextBox>
<TextBlock
Margin="0,4,0,0"
HorizontalAlignment="Center"
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
Text="{ReflectionBinding Validation.Errors[Value]}" />
</StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>
@@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace Wallet.Data; namespace Wallet.Data;
[Table("Blobs")] [Table("Blobs")]
public record BlobEntry public record BlobEntity
{ {
public byte[]? Data { get; set; } public byte[]? Data { get; set; }
@@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace Wallet.Data; namespace Wallet.Data;
[Table("Items")] [Table("Items")]
public record ItemEntry public record ItemEntity
{ {
[Key] [Key]
public Guid Id { get; set; } public Guid Id { get; set; }
@@ -17,11 +17,11 @@ public record ItemEntry
public Guid? ImageId { get; set; } public Guid? ImageId { get; set; }
public BlobEntry? Image { get; set; } public BlobEntity? Image { get; set; }
public required string Category { get; set; } public required string Category { get; set; }
public ICollection<TagEntry> Tags { get; set; } = new List<TagEntry>(); public ICollection<TagEntity> Tags { get; set; } = new List<TagEntity>();
public ICollection<BlobEntry> Blobs { get; set; } = new List<BlobEntry>(); public ICollection<BlobEntity> Blobs { get; set; } = new List<BlobEntity>();
} }
+9 -9
View File
@@ -6,11 +6,11 @@ public interface IConnection;
public class WalletContext(IConnection connection) : DbContext public class WalletContext(IConnection connection) : DbContext
{ {
public DbSet<BlobEntry> Blobs { get; set; } public DbSet<BlobEntity> Blobs { get; set; }
public DbSet<ItemEntry> Items { get; set; } public DbSet<ItemEntity> Items { get; set; }
public DbSet<TagEntry> Tags { get; set; } public DbSet<TagEntity> Tags { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
@@ -19,27 +19,27 @@ public class WalletContext(IConnection connection) : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Entity<ItemEntry>() modelBuilder.Entity<ItemEntity>()
.HasKey(x => x.Id); .HasKey(x => x.Id);
modelBuilder.Entity<ItemEntry>() modelBuilder.Entity<ItemEntity>()
.HasMany(x => x.Tags) .HasMany(x => x.Tags)
.WithOne() .WithOne()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<ItemEntry>() modelBuilder.Entity<ItemEntity>()
.HasMany(x => x.Blobs) .HasMany(x => x.Blobs)
.WithOne() .WithOne()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<ItemEntry>(). modelBuilder.Entity<ItemEntity>().
HasOne(i => i.Image) HasOne(i => i.Image)
.WithOne() .WithOne()
.HasForeignKey<ItemEntry>(i => i.ImageId) .HasForeignKey<ItemEntity>(i => i.ImageId)
.IsRequired(false) .IsRequired(false)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<BlobEntry>() modelBuilder.Entity<BlobEntity>()
.HasKey(x => x.Id); .HasKey(x => x.Id);
} }
} }
@@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace Wallet.Data; namespace Wallet.Data;
[Table("Tags")] [Table("Tags")]
public class TagEntry public class TagEntity
{ {
[Key] [Key]
public Guid Id { get; set; } public Guid Id { get; set; }
+4 -4
View File
@@ -28,12 +28,12 @@ public class CreateItemHandler(IImageWriter imageWriter,
thumbData = memoryStream.ToArray(); thumbData = memoryStream.ToArray();
} }
ItemEntry itemEntry = new() ItemEntity itemEntry = new()
{ {
Id = id, Id = id,
Name = name, Name = name,
Category = category, Category = category,
Image = thumbData != null ? new BlobEntry Image = thumbData != null ? new BlobEntity
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Data = thumbData, Data = thumbData,
@@ -42,7 +42,7 @@ public class CreateItemHandler(IImageWriter imageWriter,
} : null, } : null,
Blobs = Blobs =
{ {
new BlobEntry new BlobEntity
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Data = Encoding.UTF8.GetBytes(content), Data = Encoding.UTF8.GetBytes(content),
@@ -53,7 +53,7 @@ public class CreateItemHandler(IImageWriter imageWriter,
}; };
using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken); using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken);
EntityEntry<ItemEntry>? result = await context.AddAsync(itemEntry, cancellationToken); EntityEntry<ItemEntity>? result = await context.AddAsync(itemEntry, cancellationToken);
await context.SaveChangesAsync(cancellationToken); await context.SaveChangesAsync(cancellationToken);
+1 -1
View File
@@ -15,7 +15,7 @@ public class DeleteItemHandler(IDbContextFactory<WalletContext> dbContextFactory
Guid id = item.Value; Guid id = item.Value;
using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken); using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken);
if (await context.FindAsync<ItemEntry>(id) is ItemEntry result) if (await context.FindAsync<ItemEntity>(id) is ItemEntity result)
{ {
context.Items.Remove(result); context.Items.Remove(result);
await context.SaveChangesAsync(cancellationToken); await context.SaveChangesAsync(cancellationToken);
+3
View File
@@ -0,0 +1,3 @@
namespace Wallet;
public record ItemEntry;
+6 -6
View File
@@ -19,9 +19,9 @@ public partial class ItemEntryViewModel<TValue>(IServiceProvider provider,
double width) : double width) :
Observable<string, TValue>(provider, factory, mediator, publisher, subscriber, disposer, key, value), Observable<string, TValue>(provider, factory, mediator, publisher, subscriber, disposer, key, value),
IItemEntryViewModel, IItemEntryViewModel,
INotificationHandler<UpdateEventArgs<Item>>, INotificationHandler<ConfirmEventArgs<ItemEntry>>,
INotificationHandler<ConfirmEventArgs<Item>>, INotificationHandler<UpdateEventArgs<ItemEntry>>,
INotificationHandler<CancelEventArgs<Item>> INotificationHandler<CancelEventArgs<ItemEntry>>
where TValue : notnull where TValue : notnull
{ {
[ObservableProperty] [ObservableProperty]
@@ -36,10 +36,10 @@ public partial class ItemEntryViewModel<TValue>(IServiceProvider provider,
[ObservableProperty] [ObservableProperty]
private double width = width; private double width = width;
public Task Handle(UpdateEventArgs<Item> args) => public Task Handle(UpdateEventArgs<ItemEntry> args) =>
Task.FromResult(State = ItemState.Write); Task.FromResult(State = ItemState.Write);
public Task Handle(CancelEventArgs<Item> args) public Task Handle(CancelEventArgs<ItemEntry> args)
{ {
Revert(); Revert();
@@ -47,7 +47,7 @@ public partial class ItemEntryViewModel<TValue>(IServiceProvider provider,
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task Handle(ConfirmEventArgs<Item> args) public Task Handle(ConfirmEventArgs<ItemEntry> args)
{ {
Commit(); Commit();
+2 -2
View File
@@ -16,7 +16,7 @@ public class ItemHandler(IDbContextFactory<WalletContext> dbContextFactory) :
Guid id = item.Value; Guid id = item.Value;
using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken); using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken);
var result = await context.Set<ItemEntry>() var result = await context.Set<ItemEntity>()
.Where(x => x.Id == id) .Where(x => x.Id == id)
.Select(x => new .Select(x => new
{ {
@@ -35,7 +35,7 @@ public class ItemHandler(IDbContextFactory<WalletContext> dbContextFactory) :
if (result is not null) if (result is not null)
{ {
ItemConfiguration? configuration = null; ItemConfiguration? configuration = null;
if (result.Blob is BlobEntry blob && blob.Data is { Length: > 0 } data) if (result.Blob is BlobEntity blob && blob.Data is { Length: > 0 } data)
{ {
try try
{ {
+21 -6
View File
@@ -1,14 +1,17 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using System.Xml.Linq;
using Toolkit.Foundation; using Toolkit.Foundation;
using Wallet.Data;
namespace Wallet; namespace Wallet;
public partial class ItemHeaderViewModel : public partial class ItemHeaderViewModel :
Observable<string>, Observable<string>,
INotificationHandler<UpdateEventArgs<Item>>, IHandler<ValidateEventArgs<ItemEntry>, bool>,
INotificationHandler<ConfirmEventArgs<Item>>, INotificationHandler<UpdateEventArgs<ItemEntry>>,
INotificationHandler<CancelEventArgs<Item>>, INotificationHandler<ConfirmEventArgs<ItemEntry>>,
INotificationHandler<CancelEventArgs<ItemEntry>>,
INotificationHandler<NotifyEventArgs<ItemCategory<string>>>, INotificationHandler<NotifyEventArgs<ItemCategory<string>>>,
INotificationHandler<NotifyEventArgs<Item<IImageDescriptor>>>, INotificationHandler<NotifyEventArgs<Item<IImageDescriptor>>>,
IItemViewModel IItemViewModel
@@ -30,11 +33,14 @@ public partial class ItemHeaderViewModel :
IPublisher publisher, IPublisher publisher,
ISubscriber subscriber, ISubscriber subscriber,
IDisposer disposer, IDisposer disposer,
IValidation validation,
ItemHeaderConfiguration configuration, ItemHeaderConfiguration configuration,
ItemState state, ItemState state,
string value, string value,
IImageDescriptor? imageDescriptor = null) : base(provider, factory, mediator, publisher, subscriber, disposer, value) IImageDescriptor? imageDescriptor = null) : base(provider, factory, mediator, publisher, subscriber, disposer, value)
{ {
Validation = validation;
this.configuration = configuration; this.configuration = configuration;
State = state; State = state;
@@ -43,12 +49,18 @@ public partial class ItemHeaderViewModel :
Track(nameof(Value), () => Value, x => Value = x); Track(nameof(Value), () => Value, x => Value = x);
Track(nameof(ImageDescriptor), () => ImageDescriptor, x => ImageDescriptor = x); Track(nameof(ImageDescriptor), () => ImageDescriptor, x => ImageDescriptor = x);
Validation.Add(() => Value, [new ValidationRule(() => Value is { Length: > 0 }, "Name is required")],
ValidationTrigger.Deferred);
} }
public Task Handle(UpdateEventArgs<Item> args) => [ObservableProperty]
private IValidation validation;
public Task Handle(UpdateEventArgs<ItemEntry> args) =>
Task.FromResult(State = ItemState.Write); Task.FromResult(State = ItemState.Write);
public Task Handle(CancelEventArgs<Item> args) public Task Handle(CancelEventArgs<ItemEntry> args)
{ {
Revert(); Revert();
@@ -56,7 +68,7 @@ public partial class ItemHeaderViewModel :
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task Handle(ConfirmEventArgs<Item> args) public Task Handle(ConfirmEventArgs<ItemEntry> args)
{ {
Commit(); Commit();
@@ -85,6 +97,9 @@ public partial class ItemHeaderViewModel :
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task<bool> Handle(ValidateEventArgs<ItemEntry> args,
CancellationToken cancellationToken) => await Validation.Validate();
protected override void OnValueChanged() protected override void OnValueChanged()
{ {
if (configuration is not null) if (configuration is not null)
+2 -2
View File
@@ -16,7 +16,7 @@ public class ItemImageHandler(IDbContextFactory<WalletContext> dbContextFactory,
Guid id = item.Value; Guid id = item.Value;
using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken); using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken);
var result = await context.Set<ItemEntry>() var result = await context.Set<ItemEntity>()
.Where(x => x.Id == id) .Where(x => x.Id == id)
.Select(x => new .Select(x => new
{ {
@@ -25,7 +25,7 @@ public class ItemImageHandler(IDbContextFactory<WalletContext> dbContextFactory,
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
if (result is not null && if (result is not null &&
result.Image is BlobEntry image && result.Image is BlobEntity image &&
image.Data is { Length: > 0 } data) image.Data is { Length: > 0 } data)
{ {
MemoryStream stream = new(data); MemoryStream stream = new(data);
+14 -2
View File
@@ -1,5 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Toolkit.Foundation; using Toolkit.Foundation;
using Wallet.Data;
namespace Wallet; namespace Wallet;
@@ -77,6 +78,7 @@ public partial class ItemViewModel :
public Task Handle(UpdateEventArgs<Item> args) public Task Handle(UpdateEventArgs<Item> args)
{ {
Publisher.Publish(Update.As<ItemEntry>());
Publisher.Publish(Notify.As(Factory.Create<ItemCommandHeaderCollection>(new List<IDisposable> Publisher.Publish(Notify.As(Factory.Create<ItemCommandHeaderCollection>(new List<IDisposable>
{ {
Factory.Create<ConfirmItemActionViewModel>(), Factory.Create<ConfirmItemActionViewModel>(),
@@ -89,6 +91,7 @@ public partial class ItemViewModel :
public Task Handle(CancelEventArgs<Item> args) public Task Handle(CancelEventArgs<Item> args)
{ {
Publisher.Publish(Cancel.As<ItemEntry>());
Publisher.Publish(Notify.As(Factory.Create<ItemCommandHeaderCollection>(new List<IDisposable> Publisher.Publish(Notify.As(Factory.Create<ItemCommandHeaderCollection>(new List<IDisposable>
{ {
Factory.Create<EditItemActionViewModel>(), Factory.Create<EditItemActionViewModel>(),
@@ -99,8 +102,17 @@ public partial class ItemViewModel :
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task Handle(ConfirmEventArgs<Item> args) public async Task Handle(ConfirmEventArgs<Item> args)
{ {
List<bool> results = [];
await foreach (bool result in Mediator.HandleManyAsync<ValidateEventArgs<ItemEntry>, bool>(Validate.As<ItemEntry>()))
{
results.Add(result);
}
if (results.All(result => result))
{
Publisher.Publish(Confirm.As<ItemEntry>());
Publisher.Publish(Notify.As(Factory.Create<ItemCommandHeaderCollection>(new List<IDisposable> Publisher.Publish(Notify.As(Factory.Create<ItemCommandHeaderCollection>(new List<IDisposable>
{ {
Factory.Create<FavouriteItemActionViewModel>(Favourite), Factory.Create<FavouriteItemActionViewModel>(Favourite),
@@ -112,7 +124,7 @@ public partial class ItemViewModel :
State is ItemState.New ? nameof(ItemState.New) : nameof(ItemState.Write)); State is ItemState.New ? nameof(ItemState.New) : nameof(ItemState.Write));
State = ItemState.Read; State = ItemState.Read;
return Task.CompletedTask; }
} }
public override Task OnActivated() public override Task OnActivated()
+3 -3
View File
@@ -16,8 +16,8 @@ public class QueryWalletHandler(IDbContextFactory<WalletContext> dbContextFactor
{ {
(string filter, string text) = Wallet.Value; (string filter, string text) = Wallet.Value;
ExpressionStarter<ItemEntry> predicate = ExpressionStarter<ItemEntity> predicate =
PredicateBuilder.New<ItemEntry>(true); PredicateBuilder.New<ItemEntity>(true);
if (filter is { Length: <= 0 }) if (filter is { Length: <= 0 })
{ {
@@ -49,7 +49,7 @@ public class QueryWalletHandler(IDbContextFactory<WalletContext> dbContextFactor
} }
using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken); using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken);
var results = await context.Set<ItemEntry>() var results = await context.Set<ItemEntity>()
.Where(predicate) .Where(predicate)
.Select(x => new .Select(x => new
{ {
+3 -3
View File
@@ -20,7 +20,7 @@ public class UpdateItemHander(IDbContextFactory<WalletContext> dbContextFactory,
try try
{ {
using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken); using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken);
ItemEntry? result = result = await context.Set<ItemEntry>() ItemEntity? result = result = await context.Set<ItemEntity>()
.Include(x => x.Image).FirstOrDefaultAsync(x => x.Id == id, cancellationToken); .Include(x => x.Image).FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (result is not null) if (result is not null)
@@ -40,7 +40,7 @@ public class UpdateItemHander(IDbContextFactory<WalletContext> dbContextFactory,
imageWriter.Write(imageDescriptor, memoryStream); imageWriter.Write(imageDescriptor, memoryStream);
thumbData = memoryStream.ToArray(); thumbData = memoryStream.ToArray();
if (result.Image is BlobEntry existingImageBlob) if (result.Image is BlobEntity existingImageBlob)
{ {
existingImageBlob.Data = thumbData; existingImageBlob.Data = thumbData;
existingImageBlob.DateTime = DateTime.UtcNow; existingImageBlob.DateTime = DateTime.UtcNow;
@@ -49,7 +49,7 @@ public class UpdateItemHander(IDbContextFactory<WalletContext> dbContextFactory,
} }
else else
{ {
result.Image = new BlobEntry result.Image = new BlobEntity
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Data = thumbData, Data = thumbData,
+1 -1
View File
@@ -13,7 +13,7 @@ public class UpdateItemStateHandler(IDbContextFactory<WalletContext> dbContextFa
if (args.Sender is (Guid id, int state)) if (args.Sender is (Guid id, int state))
{ {
using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken); using WalletContext context = await dbContextFactory.CreateDbContextAsync(cancellationToken);
if (await context.FindAsync<ItemEntry>(id) is ItemEntry result) if (await context.FindAsync<ItemEntity>(id) is ItemEntity result)
{ {
result.State = state; result.State = state;
await context.SaveChangesAsync(cancellationToken); await context.SaveChangesAsync(cancellationToken);