Skip to content

Commit

Permalink
BookLib: Use async methods for database access; improve error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
jbe2277 committed Nov 30, 2024
1 parent 11f51ac commit ec393ca
Show file tree
Hide file tree
Showing 13 changed files with 143 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Waf.Applications;
using System.Waf.UnitTesting;
using System.Waf.UnitTesting.Mocks;
using Waf.BookLibrary.Library.Applications.Controllers;
using Waf.BookLibrary.Library.Applications.Services;
using Waf.BookLibrary.Library.Applications.ViewModels;
using Waf.BookLibrary.Library.Domain;

namespace Test.BookLibrary.Library.Applications.Controllers;

[TestClass]
public class EntityControllerTest : ApplicationsTest
{
[TestMethod]
public void ValidateBeforeSave()
public async Task ValidateBeforeSave()
{
var controller = Get<EntityController>();
controller.Initialize();
Expand All @@ -23,7 +23,7 @@ public void ValidateBeforeSave()
Assert.IsTrue(controller.HasChanges());

Assert.IsTrue(controller.CanSave());
controller.Save();
Assert.IsTrue(await controller.SaveCore());
Assert.IsFalse(controller.HasChanges());

var messageService = Get<MockMessageService>();
Expand All @@ -33,7 +33,7 @@ public void ValidateBeforeSave()
entityService.Persons[^1].Validate();
Assert.IsTrue(controller.HasChanges());
var shellViewModel = Get<ShellViewModel>();
shellViewModel.SaveCommand!.Execute(null);
await ((AsyncDelegateCommand)shellViewModel.SaveCommand!).ExecuteAsync(null);

Assert.AreEqual(MessageType.Error, messageService.MessageType);
Assert.IsTrue(controller.HasChanges());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,19 @@ public class MockEntityController : IEntityController

public bool HasChangesResult { get; set; }

public bool CanSaveResult { get; set; }
public bool CanSaveResult { get; set; } = true;

public bool SaveResult { get; set; }
public Func<Task<bool>>? SaveCoreStub { get; set; }

public bool SaveCalled { get; set; }

public MockEntityController() => CanSaveResult = true;
public bool SaveCoreCalled { get; set; }

public void Initialize() => InitializeCalled = true;

public bool HasChanges() => HasChangesResult;

public bool CanSave() => CanSaveResult;

public bool Save()
{
SaveCalled = true;
return SaveResult;
}
public Task<bool> SaveCore() => SaveCoreStub?.Invoke() ?? Task.FromResult(true);

public void Shutdown() => ShutdownCalled = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,33 +67,35 @@ public void ModuleControllerHasChangesTest()
return true;
};
// Then we simulate that the EntityController wasn't able to save the changes.
entityController.SaveResult = false;
var saveCalled = false;
entityController.SaveCoreStub = () =>
{
saveCalled = true;
return Task.FromResult(false);
};
shellViewModel.ExitCommand!.Execute(null);
// The Save method must be called. Because the save operation failed the expect the ShellView to be
// still visible.
Assert.IsTrue(entityController.SaveCalled);
// The Save method must be called. Because the save operation failed the expect the ShellView to be still visible.
Assert.IsTrue(saveCalled);
Assert.IsTrue(shellView.IsVisible);

// Exit the application although we have unsaved changes.
entityController.HasChangesResult = true;
entityController.SaveCalled = false;
saveCalled = false;
// When the question box asks us to save the changes we say "Cancel" => null.
messageService.ShowQuestionStub = (_, _) => null;
// This time the Save method must not be called. Because we have chosen "Cancel" the ShellView must still
// be visible.
// This time the Save method must not be called. Because we have chosen "Cancel" the ShellView must still be visible.
shellViewModel.ExitCommand.Execute(null);
Assert.IsFalse(entityController.SaveCalled);
Assert.IsFalse(saveCalled);
Assert.IsTrue(shellView.IsVisible);

// Exit the application although we have unsaved changes.
entityController.HasChangesResult = true;
entityController.SaveCalled = false;
saveCalled = false;
// When the question box asks us to save the changes we say "No" => false.
messageService.ShowQuestionStub = (_, _) => false;
// This time the Save method must not be called. Because we have chosen "No" the ShellView must still
// be closed.
// This time the Save method must not be called. Because we have chosen "No" the ShellView must still be closed.
shellViewModel.ExitCommand.Execute(null);
Assert.IsFalse(entityController.SaveCalled);
Assert.IsFalse(saveCalled);
Assert.IsFalse(shellView.IsVisible);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.ComponentModel.Composition;
using System.Waf.Applications;
using System.Waf.Applications.Services;
using Waf.BookLibrary.Library.Applications.DataModels;
using Waf.BookLibrary.Library.Applications.Properties;
using Waf.BookLibrary.Library.Applications.Services;
using Waf.BookLibrary.Library.Applications.ViewModels;
using Waf.BookLibrary.Library.Domain;
Expand All @@ -11,6 +13,7 @@ namespace Waf.BookLibrary.Library.Applications.Controllers;
[Export]
internal class BookController
{
private readonly IMessageService messageService;
private readonly IShellService shellService;
private readonly IEntityService entityService;
private readonly BookListViewModel bookListViewModel;
Expand All @@ -22,8 +25,10 @@ internal class BookController
private SynchronizingList<BookDataModel, Book>? bookDataModels;

[ImportingConstructor]
public BookController(IShellService shellService, IEntityService entityService, BookListViewModel bookListViewModel, BookViewModel bookViewModel, ExportFactory<LendToViewModel> lendToViewModelFactory)
public BookController(IMessageService messageService, IShellService shellService, IEntityService entityService, BookListViewModel bookListViewModel,
BookViewModel bookViewModel, ExportFactory<LendToViewModel> lendToViewModelFactory)
{
this.messageService = messageService;
this.shellService = shellService;
this.entityService = entityService;
this.bookListViewModel = bookListViewModel;
Expand All @@ -36,7 +41,7 @@ public BookController(IShellService shellService, IEntityService entityService,

internal ObservableListViewCore<BookDataModel>? BooksView { get; private set; }

public void Initialize()
public async void Initialize()
{
bookViewModel.LendToCommand = lendToCommand;
bookViewModel.PropertyChanged += BookViewModelPropertyChanged;
Expand All @@ -51,6 +56,15 @@ public void Initialize()
shellService.BookListView = bookListViewModel.View;
shellService.BookView = bookViewModel.View;

try
{
await entityService.LoadBooks();
}
catch (Exception ex)
{
Log.Default.Error(ex, "LoadBooks");
messageService.ShowError(shellService.ShellView, Resources.LoadErrorBooks);
}
bookListViewModel.SelectedBook = bookListViewModel.Books.FirstOrDefault();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal class EntityController : IEntityController
private readonly IShellService shellService;
private readonly IDBContextService dBContextService;
private readonly Lazy<ShellViewModel> shellViewModel;
private readonly DelegateCommand saveCommand;
private readonly AsyncDelegateCommand saveCommand;
private DbContext? bookLibraryContext;

[ImportingConstructor]
Expand All @@ -29,7 +29,7 @@ public EntityController(EntityService entityService, IMessageService messageServ
this.shellService = shellService;
this.dBContextService = dBContextService;
this.shellViewModel = shellViewModel;
saveCommand = new(() => Save(), CanSave);
saveCommand = new(Save, CanSave);
}

private ShellViewModel ShellViewModel => shellViewModel.Value;
Expand All @@ -50,7 +50,22 @@ public void Initialize()

public bool CanSave() => ShellViewModel.IsValid;

public bool Save()
public async Task<bool> SaveCore()
{
Log.Default.Info("Save changes in database.");
try
{
await entityService.SaveChanges().ConfigureAwait(false);
return true;
}
catch (Exception ex)
{
Log.Default.Error(ex, "SaveChangesAsync");
}
return false;
}

private async Task Save()
{
var entities = bookLibraryContext!.ChangeTracker.Entries().Where(x => x.State == EntityState.Added || x.State == EntityState.Modified).Select(x => x.Entity).ToArray();
var errors = entities.OfType<ValidatableModel>().Where(x => x.HasErrors).ToArray();
Expand All @@ -59,12 +74,13 @@ public bool Save()
var errorMessages = errors.Select(x => string.Format(CultureInfo.CurrentCulture, Resources.EntityInvalid, EntityToString(x), string.Join(Environment.NewLine, x.Errors)));
Log.Default.Warn("Abort save changes because of errors: {0}", string.Join("; ", errorMessages));
messageService.ShowError(shellService.ShellView, Resources.SaveErrorInvalidEntities, string.Join(Environment.NewLine, errorMessages));
return false;
return;
}

Log.Default.Info("Save changes in database.");
bookLibraryContext.SaveChanges();
return true;
if (!await SaveCore())
{
messageService.ShowError(shellService.ShellView, Resources.SaveErrorDatabase);
}
}

private void ShellViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ internal interface IEntityController

bool CanSave();

bool Save();
Task<bool> SaveCore();

void Shutdown();
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ private void ShellViewModelClosing(object? sender, CancelEventArgs e)
bool? result = messageService.ShowQuestion(shellService.ShellView, Resources.SaveChangesQuestion);
if (result == true)
{
if (!entityController.Save())
if (!entityController.SaveCore().GetAwaiter().GetResult())
{
messageService.ShowError(shellService.ShellView, Resources.SaveErrorDatabase);
e.Cancel = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,30 @@ public PersonController(IMessageService messageService, IShellService shellServi

internal ObservableListViewCore<Person>? PersonsView { get; private set; }

public void Initialize()
public async void Initialize()
{
personViewModel.CreateNewEmailCommand = createNewEmailCommand;
personViewModel.PropertyChanged += PersonViewModelPropertyChanged;

PersonsView = new(entityService.Persons, null, personListViewModel.Filter, null);
personListViewModel.Persons = PersonsView;
personListViewModel.AddNewCommand = addNewCommand;
personListViewModel.RemoveCommand = removeCommand;
personListViewModel.CreateNewEmailCommand = createNewEmailCommand;
personListViewModel.PropertyChanged += PersonListViewModelPropertyChanged;

shellService.PersonListView = personListViewModel.View;
shellService.PersonView = personViewModel.View;

try
{
await entityService.LoadPersons();
}
catch (Exception ex)
{
Log.Default.Error(ex, "LoadPersons");
messageService.ShowError(shellService.ShellView, Resources.LoadErrorPersons);
}
personListViewModel.SelectedPerson = personListViewModel.Persons.FirstOrDefault();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
[assembly: SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Scope = "type", Target = "~T:Waf.BookLibrary.Library.Applications.Controllers.EntityController")]
[assembly: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "BookList", Scope = "type", Target = "~T:Waf.BookLibrary.Library.Applications.ViewModels.BookListViewModel")]
[assembly: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "BookList", Scope = "type", Target = "~T:Waf.BookLibrary.Library.Applications.Views.IBookListView")]
[assembly: SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "CanSave", Scope = "member", Target = "~M:Waf.BookLibrary.Library.Applications.Controllers.EntityController.Save~System.Boolean")]
[assembly: SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Scope = "member", Target = "~M:Waf.BookLibrary.Library.Applications.ViewModels.PersonListViewModel.Filter(Waf.BookLibrary.Library.Domain.Person)~System.Boolean")]
[assembly: SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Scope = "member", Target = "~M:Waf.BookLibrary.Library.Applications.ViewModels.BookListViewModel.Filter(Waf.BookLibrary.Library.Applications.DataModels.BookDataModel)~System.Boolean")]
[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "~M:Waf.BookLibrary.Library.Applications.Controllers.ModuleController.Shutdown")]
Expand All @@ -16,4 +15,4 @@
[assembly: SuppressMessage("Performance", "CA1826:Do not use Enumerable methods on indexable collections", Justification = "<Pending>", Scope = "member", Target = "~M:Waf.BookLibrary.Library.Applications.Controllers.PersonController.Initialize")]
[assembly: SuppressMessage("Performance", "CA1826:Do not use Enumerable methods on indexable collections", Justification = "<Pending>", Scope = "member", Target = "~M:Waf.BookLibrary.Library.Applications.Controllers.PersonController.RemovePerson")]
[assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "<Pending>", Scope = "member", Target = "~M:Waf.BookLibrary.Library.Applications.ViewModels.ShellViewModel.#ctor(Waf.BookLibrary.Library.Applications.Views.IShellView,System.Waf.Applications.Services.IMessageService,Waf.BookLibrary.Library.Applications.Services.IShellService,System.Waf.Applications.Services.ISettingsService)")]
[assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "<Pending>", Scope = "member", Target = "~M:Waf.BookLibrary.Library.Applications.Controllers.EntityController.Save~System.Boolean")]
[assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "<Pending>", Scope = "member", Target = "~M:Waf.BookLibrary.Library.Applications.Controllers.EntityController.Save~System.Threading.Tasks.Task")]

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,13 @@ https://github.com/jbe2277/waf

{0}</value>
</data>
<data name="SaveErrorDatabase" xml:space="preserve">
<value>The save operation failed.</value>
</data>
<data name="LoadErrorBooks" xml:space="preserve">
<value>Could not load the Books from the database.</value>
</data>
<data name="LoadErrorPersons" xml:space="preserve">
<value>Could not load the Persons from the database.</value>
</data>
</root>
Loading

0 comments on commit ec393ca

Please sign in to comment.