Giter Site home page Giter Site logo

baseflow / guardedactions Goto Github PK

View Code? Open in Web Editor NEW
13.0 8.0 3.0 782 KB

A library to increase the error handling, testability and reusability for all your MVVM driven apps!

Home Page: https://baseflow.com

License: MIT License

C# 100.00%
xamarin mvvm commands netcore xamarinforms

guardedactions's Introduction

GuardedActions

A library to πŸš€ increase the ❌ error handling, πŸ§ͺ testability and ♻️ reusability for all your MVVM driven apps!

So why should I use it?

Like said before this library helps your to increase the error handling, testability and reusability of all of your MVVM driven apps. Now let's see how exactly this library will help you with all of these things!

Reusability

The command builders make it really easy to reuse commands troughout multiple view models without having to create some kind of base class like shown below:

Classic example:

public class ViewModelA : SharedViewModel
{
}

public class ViewModelB : SharedViewModel
{
}

public class SharedViewModel : BaseViewModel
{
    private ICommand _scanCommand;
    public ICommand ScanCommand => _scanCommand ??= new Command(Scan);
    private void Scan()
    {
        ...
    }
}

The solution above could cause some issues in bigger projects. In these projects people tend to reuse specific commands. The most common approach we see people take is creating a base class containing all the shared commands.

Now in big projects this shared class tend to grow quickly and become a big 'spagetti' class with all kind of commands for different purposes..

Not really following the separation of concerns design priciples..

So, this means that if you only need one or a couple of the commands you'll need to inherit the entire base class with all kinds of stuff you don't want/need or you'll create a copy with only those commands you like to use. But keep in mind when you copy past commands you don't reuse them..

So with the Guarded actions you could solve this issue by creating CommandBuilders which can be loaded trough DI and so are reuseable. Note: you don't need to register the builders yourself if you install the GuardedActions library correctly it'll register and resolve the builders automatically.

GuardedAction example:

    public class ViewModelA : BaseViewModel, IScannable
    {
        private readonly IScanCommandBuilder _scanCommandBuilder;
        
        private ICommand _scanCommand;
        public ICommand ScanCommand => _scanCommand ??= _scanCommandBuilder?.RegisterDataContext(this).BuildCommand()
        
        public ViewModelA(IScanCommandBuilder scanCommandBuilder)
        {
            _scanCommandBuilder = scanCommandBuilder ?? throw new ArgumentNullException(nameof(scanCommandBuilder));
        }
    }
    
    public class ViewModelB : BaseViewModel, IScannable
    {
        private readonly IScanCommandBuilder _scanCommandBuilder;
        
        private ICommand _scanCommand;
        public ICommand ScanCommand => _scanCommand ??= _scanCommandBuilder?.RegisterDataContext(this).BuildCommand()
        
        public ViewModelA(IScanCommandBuilder scanCommandBuilder)
        {
            _scanCommandBuilder = scanCommandBuilder ?? throw new ArgumentNullException(nameof(scanCommandBuilder));
        }
    }
    
    public class ScanCommandBuilder : AsyncGuardedDataContextCommandBuilder<IScannable>, IScanCommandBuilder
    {
        protected override Task ExecuteCommandAction()
        {
            // 1. Multiple actions can be added handling the scanning feature
            // 2. The DataContext could be modified directly. This is in this case the ViewModel but accessed trough it's IScannable contract.
            return Task.CompletedTask;
        }
    }

Testability

If you use this library in the way it's designed to be used you should end up having loads of small building blocks:

  • ViewModels
  • Command(Builder)s
  • Actions
  • ErrorHandlers

Each of these buidling blocks should be isolated, loosely coupled and obey the seperation of concerne design priciples. This will mean that you only have to write really small tests with a clear goal wich will greatly improve the testability in your project.

So, for example if you seperate all the commands from the view models the view model should only contain some bindable properties with the pupose of storing and displaying data on the view. This results in the view model having one clear goal and that one clear goal becomes easily testable. See the example below:

GuardedAction - View model example:

public class MainViewModel : BaseViewModel
{
    private string _errorMessage;

    private ObservableCollection<Download> _downloads;

    private readonly IInitializeCommandBuilder _initializeCommandBuilder;
    private readonly IDownloadAllCommandBuilder _downloadAllCommandBuilder;

    private ICommand _initializeCommand;
    private ICommand _downloadAllCommand;

    public MainViewModel(IInitializeCommandBuilder initializeCommandBuilder, IDownloadAllCommandBuilder downloadAllCommandBuilder)
    {
        _initializeCommandBuilder = initializeCommandBuilder ?? throw new ArgumentNullException(nameof(initializeCommandBuilder));
        _downloadAllCommandBuilder = downloadAllCommandBuilder ?? throw new ArgumentNullException(nameof(downloadAllCommandBuilder));

        InitializeCommand.Execute(null);
    }

    public ICommand InitializeCommand => _initializeCommand ??= _initializeCommandBuilder?.RegisterDataContext(this).BuildCommand();
    public ICommand DownloadAllCommand => _downloadAllCommand ??= _downloadAllCommandBuilder?.RegisterDataContext(this).BuildCommand();

    public bool ShowErrorMessage => ErrorMessage != null;

    public string ErrorMessage
    {
        get => _errorMessage;
        set => SetProperty(ref _errorMessage, value, () => RaisePropertyChanged(nameof(ShowErrorMessage)));
    }

    public ObservableCollection<Download> Downloads
    {
        get => _downloads;
        set => SetProperty(ref _downloads, value);
    }
}

GuardedAction - View model test example:

[Fact]
public void MainViewModel_InheritsBaseViewModel()
{
    // Arrange
    var baseViewModelType = typeof(BaseViewModel);
    var mainViewModelType = typeof(MainViewModel);

    // Act
    var result = baseViewModelType.IsAssignableFrom(mainViewModelType);

    // Assert
    Assert.True(result);
}

[Fact]
public void MainViewModel_CheckNotifyPropertyChanged()
{
    // Arrange
    var initializeCommandBuilder = Mock.Of<IInitializeCommandBuilder>();
    var downloadAllCommandBuilder = Mock.Of<IDownloadAllCommandBuilder>();
    var viewModel = new MainViewModel(initializeCommandBuilder, downloadAllCommandBuilder);

    var lastChangedPropertyName = string.Empty;
    viewModel.PropertyChanged += (sender, args) => lastChangedPropertyName = args.PropertyName;

    // Act
    viewModel.Downloads = new ObservableCollection<Download>();

    // Assert
    Assert.Equal(nameof(viewModel.Downloads), lastChangedPropertyName);
}

[Fact]
// Check if the view receives a notification that also the ShowErrorMessage has been updated when setting the value of the ErrorMessage
public void MainViewModel_EditErrorMessage()
{
    // Arrange
    var initializeCommandBuilder = Mock.Of<IInitializeCommandBuilder>();
    var downloadAllCommandBuilder = Mock.Of<IDownloadAllCommandBuilder>();
    var viewModel =  new MainViewModel(initializeCommandBuilder, downloadAllCommandBuilder);

    var propertyChanges = new List<string>();
    viewModel.PropertyChanged += (sender, args) => propertyChanges.Add(args.PropertyName);

    // Act
    viewModel.ErrorMessage = "Foo";

    // Assert
    Assert.Equal(2, propertyChanges.Count);
    Assert.Contains(nameof(viewModel.ErrorMessage), propertyChanges);
    Assert.Contains(nameof(viewModel.ShowErrorMessage), propertyChanges);
}

Error handling

We all know how annoying it is when an app crashes and you've to start it again and navigate to the specific page that you were working on..

Well, in this kind of situation the GuardedActions library also comes in handy!

Because the GuardedActions library executes your command/action code trough the ExceptionGuard which automatically wrappes your command or action code in a try catch block.

public async Task Guard(object sender, Func<Task> job, Func<Task> onFinally = null)
{
    try
    {
        if (job == null)
            throw new InvalidOperationException($"{GetType().FullName}.{nameof(Guard)}(): The {nameof(job)} provided cannot be null.");

        await job();
    }
    catch (Exception exception)
    {
        await HandleException(sender, exception);
    }
    finally
    {
        if (onFinally != null)
        {
            await onFinally();
        }
    }
}

And if you take a good look at the example above it contains the HandleException method. This method will search for valid exception handlers to handle the exception that has occured.

Type of exception handlers

There are three different types of exception handlers handlers.

  1. Fallback
  2. Default
  3. Command/Action specific

The fallback exception handler is the one defined in the GuardedActions library and will be the ExceptionHandler to use if all other handlers have been executed. Note: it's possible to let an exception handler to stop the executution of the rest of the exception handlers.

The default exception handlers are the handlers that are not configured to be an action or command specific. Note: it's possible to let an exception handler to stop the executution of the rest of the exception handlers.

[DefaultExceptionHandler]
public class NotImplementedExceptionHandler : ExceptionHandler<NotImplementedException>
{
    private IDialogService _dialogService;

    // TODO: add ability to load use DI
    protected IDialogService DialogService => _dialogService ??= IoCRegistration.Instance.GetService<IDialogService>();

    public override Task Handle(IExceptionHandlingAction<NotImplementedException> exceptionHandlingAction)
    {
        if (exceptionHandlingAction == null) throw new ArgumentNullException(nameof(exceptionHandlingAction));

        exceptionHandlingAction.HandlingShouldFinish = true;

        var message = exceptionHandlingAction?.Exception?.Message ?? "This is not implemented yet!";

        return DialogService.Alert(message, "Coming soonβ„’", "Ok, I will wait");
    }
}

The command or action specific exception handlers will only be resolved and handled when an error has occured in the command or action on which it's specified for. Note: it's possible to let an exception handler to stop the executution of the rest of the exception handlers.

[ExceptionHandlerFor(typeof(IDownloadUrlAction))]
public class DownloadUrlActionUriFormatExceptionHandler : ContextExceptionHandler<UriFormatException, Models.DownloadableUrl>
{
    public override Task Handle(IExceptionHandlingAction<UriFormatException, Models.DownloadableUrl> exceptionHandlingAction)
    {
        if (exceptionHandlingAction?.DataContext != null)
        {
            exceptionHandlingAction.DataContext.ErrorMessage = "Not an valid URL.";
        }
        return Task.CompletedTask;
    }
}

Order of execution

The different type of exception handlers will be executed in a specific order:

  1. The command or action specific
  2. The default
  3. The fallback

Stop error handlers executing

It could be possible that an exception handler already handles everything needed and the execution of any other exception handlers will only cause duplicate data to be logged or unecessary resources to be used.

In this situation you could use the HandlingShouldFinish in the exception handler and it'll prevent any other exception handlers from being executed.

public override Task Handle(IExceptionHandlingAction<Exception, Models.DownloadableUrl> exceptionHandlingAction)
{
    // Logic to handle the exception

    exceptionHandlingAction.HandlingShouldFinish = true;
}

Supported IoC containers

The GuardedActions library comes with a set of providers to support some of the most commonly used of the IoC containers. Also it'll provide you with the option of creating your own IoC provider to allow you to connect GuardedActions to your favorite IoC container of choice.

IoC container Supported
.NET Core βœ…
MvvmCross βœ…
Unity 🚧
Autofac 🚧
Custom (read more) βœ…

Installation

Different IoC containers need different providers and so different NuGet packages. Down here you'll see samples on how to setup each IoC container provider.

🚧 The rest is to coming soon! 🚧

.NET Core

Grab the latest GuardedActions.NetCore NuGet package and install in your solution.

Install-Package GuardedActions.NetCore

Then the only thing you've to do is configuring and connect the GuardedActions library on the host builder. See the example below:

using GuardedActions.NetCore;
using GuardedActions.NetCore.Extensions;

public class Startup
{
    public static void Init()
    {
        var iocSetup = new GuardedActionsIoCSetup();

        var host = new HostBuilder()
            .ConfigureGuardedActions(iocSetup, "YourAssembliesStartWith")
            .Build()
            .ConnectGuardedActions(iocSetup);
    }
}

MvvmCross

Grab the latest GuardedActions.MvvmCross NuGet package and install in your solution.

Install-Package GuardedActions.MvvmCross

Then the only thing you've to do is configuring GuardedActions before registering the AppStart. See the example below:

using GuardedActions.MvvmCross;

public class App : MvxApplication
{
    public override void Initialize()
    {
        new GuardedActionsIoCSetup().Configure(Mvx.IoCProvider, "YourAssembliesStartWith");

        RegisterAppStart<MainViewModel>();
    }
}

Custom

Grab the latest GuardedActions NuGet package and install in your solution.

Install-Package GuardedActions

Then the only thing you've to do is to create your own IoC setup class in which you will connect your IoC container to the GuardedActions library. This can be done by creating a GuardedActionCustomIoCSetup class which inherits from the GuardedActions.IoC.IoCRegistraton class. See the example below:

using GuardedActions.IoC;

public class GuardedActionCustomIoCSetup : IoCRegistration
{
    private IYourIoCContainer? _yourIoCContainer;

    private static string _customErrorMessage = $"Make sure you've called the {nameof(Configure)} on the {nameof(GuardedActionCustomIoCSetup)} before your app starts.";

    public void Configure(IYourIoCContainer yourIoCContainer, params string[] assemblyNames)
    {
        _yourIoCContainer = yourIoCContainer ?? throw new ArgumentNullException(nameof(yourIoCContainer));
            
        Register(assemblyNames);
    }

    public override void AddSingletonInternal<TServiceType>(Func<TServiceType> constructor) where TServiceType : class => _yourIoCContainer.AddSingleton(() => constructor.Invoke());

    public override void AddSingletonInternal(Type serviceType) => _yourIoCContainer.AddSingleton(serviceType);

    public override void AddSingletonInternal(Type contractType, Type serviceType) => _yourIoCContainer.AddSingleton(contractType, serviceType);

    public override void AddTransientInternal(Type serviceType) => _yourIoCContainer.AddTransient(serviceType);

    public override void AddTransientInternal(Type contractType, Type serviceType) => _yourIoCContainer.AddTransient(contractType, serviceType);

    public override TServiceType GetServiceInternal<TServiceType>() where TServiceType : class => _yourIoCContainer.GetService<TServiceType>();

    public override TServiceType GetServiceInternal<TServiceType>(Type serviceType) where TServiceType : class => _yourIoCContainer.GetService<TServiceType>(serviceType);

    public override bool CanRegister => _yourIoCContainer != null;

    public override bool CanResolve => _yourIoCContainer != null;

    public override string CannotRegisterErrorMessage => _customErrorMessage;

    public override string CannotResolveErrorMessage => _customErrorMessage;
}

And then of course don't forget to call your custom IoC setup class after setting up your IoC container and before loading your app.

new GuardedActionCustomIoCSetup().Configure(yourIoCContainer, "YourAssembliesStartWith");

Filing issues

When filing issues, please select the appropriate issue template. The best way to get your bug fixed is to be as detailed as you can be about the problem. Providing a minimal git repository with a project showing how to reproduce the problem is ideal. Here are a couple of questions you can answer before filing a bug.

  1. Did you include a snippet of the broken code in the issue?
  2. Can you reproduce the problem in a brand new project?
  3. What are the EXACT steps to reproduce this problem?
  4. What platform(s) are you experiencing the problem on?

Remember GitHub issues support markdown. When filing bugs please make sure you check the formatting of the issue before clicking submit.

Contributing code

We are happy to receive Pull Requests adding new features and solving bugs. As for new features, please contact us before doing major work. To ensure you are not working on something that will be rejected due to not fitting into the roadmap or ideal of the library.

Git setup

Since Windows and UNIX-based systems differ in terms of line endings, it is a very good idea to configure git autocrlf settings.

On Windows we recommend setting core.autocrlf to true.

git config --global core.autocrlf true

On Mac we recommend setting core.autocrlf to input.

git config --global core.autocrlf input

Code style guidelines

Please use 4 spaces for indentation.

Otherwise default ReSharper C# code style applies.

Project Workflow

Our workflow is loosely based on Github Flow. We actively do development on the develop branch. This means that all pull requests by contributors need to be develop and requested against the develop branch. The master branch contains tags reflecting what is currently on NuGet.org.

Submitting Pull Requests

Make sure you can build the code. Familiarize yourself with the project workflow and our coding conventions. If you don't know what a pull request is read this https://help.github.com/articles/using-pull-requests.

Before submitting a feature or substantial code contribution please discuss it with the team and ensure it follows the GuardedAction roadmap. Note that code submissions will be reviewed and tested. Only code that meets quality and design/roadmap appropriateness will be merged into the source. Don't "Push" Your Pull Requests

Acknowledgements

  • Thanks to Artur Malendowicz for some ideas / code on which this library is based upon.

guardedactions's People

Contributors

jopmiddelkamp avatar martijn00 avatar orzech85 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.