Blazor et Redux

RazorBlazorRedux
Jean-Baptiste Raulin - 10/10/2018 à 14:38:080 commentaire

Si le Framework Blazor associé aux langages C# et Razor offre une solution satisfaisante, le développement Web a bien évolué ces dernières années. L'émergence du développement des applications Web en Single Pages Applications (SPA) ainsi que l'utilisation accrue des nouveaux frameworks comme Angular, React ou Vue ont amené à de nouvelles implémentations structurantes dans les projets Web. Le framework d'architecture qui nous intéresse ici, Redux, est une bibliothèque javascript populaire. Il est principalement utilisé par avec React mais peut être intégré dans tout projet Javascript. Redux est basé sur 3 principes.


Une seule source de confiance

Les données qui définissent l'application sont centralisées dans un seul objet, l'objet état (State). Cela permet de simplement mettre à jour l'application et de s'appuyer sur un jeu de données unique et accessible.


L'état est en lecture seule

On ne peut pas changer directement l'état mais seulement le mettre à jour via des actions définies. Cela évite de désordonner la mise à jour de l'état et permet de simplement rendre compte des mises à jour en monitorant les actions. Cela simplifie la maintenabilité du code.


Les modifications sont faites par des fonctions pures

Les changements ne sont effectués que via des fonctions pures, c'est-à-dire des fonctions dont le résultat ne dépend que des arguments et qui n'ont pas d'effets de bord. Cela permet de centraliser les états et de mieux répercuter leurs changements. Son fonctionnement unidirectionnel impose une mise à jour de l'état par action, ce qui a pour effet de découpler le code lié à l'affichage de celui lié au fonctionnement. Cela permet également de faire fonctionner ses composants indépendamment les uns des autres et de faciliter les tests par type d'objet. Pour plus d'informations : https://redux.js.org/

Même si Blazor peut appeler du Javascript à travers des l'API d'interopérabilité, Une implémentation native de redux en C# est souhaitable.


Bladux

On a donc implémenté Redux en C# en créant les classes suivantes.

Une classe de base pour les actions :

    public class Action<TActionTypeEnum> where TActionTypeEnum : struct, IConvertible

Une classe de base et une interface pour les reducers

    public abstract class ReducerBase<TState, TAction, TActionTypeEnum> : IReducer<TState, TAction, TActionTypeEnum>
        where TAction : Action<TActionTypeEnum>
        where TActionTypeEnum : struct, IConvertible

    public interface IReducer<TState, TAction, TActionTypeEnum>
        where TAction : Action<TActionTypeEnum>
        where TActionTypeEnum : struct, IConvertible

Une classe de base et une interface pour les stores

    public class Store<TState, TReducer, TAction, TActionTypeEnum> : IStore<TState, TAction, TActionTypeEnum>
        where TReducer : IReducer<TState, TAction, TActionTypeEnum>
        where TAction : Action<TActionTypeEnum>
        where TActionTypeEnum : struct, IConvertible

    public interface IStore<TState, TAction, TActionTypeEnum>
        where TAction : Action<TActionTypeEnum>
        where TActionTypeEnum : struct, IConvertible

Prenons un exemple d'utilisation avec un projet de notes Todo.


Action

Dans Redux, les actions permettent de passer des informations de paramétrage de l'action concernée. On créé une énumération qui nous permettra d'identifier quel type d'action est concerné

    public enum ActionTypes
    {
        None,
        AddTodo,
        ToggleTodo,
        SetVisibilityFilter
    }

Sur une action de filtrage, on va renseigner quel type de filtre de type Todo on veut appliquer. l'action hérite de la classe Action et spécifie le type de l'énumération d'action concernée

    public class FilterAction : Action<ActionTypes>
    {
        public FilterTodoTypes Filter { get; set; }
    }

Reducers

La classe de Reducer permet de traiter les actions en mettant à jour l'état. Il faut bien faire attention à ne pas écraser une donnée. Si la valeur d'un élément de l'état est modifié, il faut instancier un nouvel objet. Les reducers du projet héritent de ReducerBase en spécifiant le type de l'état, le type de l'action passée et le type de l'énumération des actions.

Ci dessous le reducer de l'état d'un todo.

    public class TodoReducer : ReducerBase<Todo, TodoAction, ActionTypes>
    {
        public override async Task<Todo> Reduce(Todo state, TodoAction action)
        {
            switch (action.Type)
            {
                case ActionTypes.AddTodo:
                    return
                    new Todo
                    {
                        Id = action.Value.Id,
                        Text = action.Value.Text,
                        IsCompleted = action.Value.IsCompleted
                    };
                case ActionTypes.ToggleTodo:
                    if (state.Id != action.Value.Id)
                        return state;
                    else
                        return new Todo
                        {
                            Id = state.Id,
                            Text = state.Text,
                            IsCompleted = !state.IsCompleted
                        };

                default:
                    return state;
            }
        }
    }
}

Les reducers peuvent appeler d'autres reducers, ce qui permet de séparer fonctionnellement les actions et garder une cohérence d'organisation du projet.

    public class TodosReducer : ReducerBase<Todo[], TodoAction, ActionTypes>
    {
        public override async Task<Todo[]> Reduce(Todo[] state, TodoAction action)
        {
            switch (action.Type)
            {
                case ActionTypes.AddTodo:
                    return state.Concat(new Todo[] { await new TodoReducer().Reduce(null, action) }).ToArray();

                case ActionTypes.ToggleTodo:
                    return state.Select(async s => (await new TodoReducer().Reduce(s, action))).Select(s => s.Result).ToArray();

                default:
                    return state;
            }
        }
    }

    public class TodoAppReducer : ReducerBase<TodoAppState, Action<ActionTypes>, ActionTypes>
    {
        public override async Task<TodoAppState> Reduce(TodoAppState state, Action<ActionTypes> action)
        {
            if (state == null)
                state = new TodoAppState();
            return new TodoAppState
            {
                Todos = (action is TodoAction ? await new TodosReducer().Reduce(state.Todos, (TodoAction)action) : state.Todos),
                VisibilityFilter = (action is FilterAction ? await new VisibilityFilterReducer().Reduce(state.VisibilityFilter, (FilterAction)action) : state.VisibilityFilter),
            };
        }
    }

State

On créé une classe qui constituera notre state. Il contient notre liste de tâches et le paramètre de visibilité

    public class TodoAppState
    {
        public Todo[] Todos { get; set; }
        public FilterTodoTypes VisibilityFilter { get; set; }

        public TodoAppState()
        {
            Todos = new Todo[0];
            VisibilityFilter = FilterTodoTypes.ShowAll;
        }
    }

Store

On instancie le store dans une classe statique qui l'appellera au besoin.

Le store manager instancie la classe Store en spécifiant le type de l'état, le type du reducer, Le type d'action et le type de l'énumération des actions concernés.

    public static class StoreManager
    {
        private static IStore<TodoAppState, Action<ActionTypes>, ActionTypes> _store = new Store<TodoAppState, TodoAppReducer, Action<ActionTypes>, ActionTypes>();

        public static IStore<TodoAppState, Action<ActionTypes>, ActionTypes> Store { get { return _store; } }
    }

Il ne nous reste plus qu'à faire notre page elle même.

L'inscription de la fonction Render au store permet de gérer la mise à jour de l'affichage en fonction de celle de l'état.

@page "/"@using Bladux.Test.TodoList.Shared.Redux

<h1>Todo List</h1>

@if (StoreManager.Store != null)
{
    <input type="text" bind="@iVal" />
    <button onclick="@(() =>
            {
                StoreManager.Store.Dispatch(new TodoAction
                {
                    Type = ActionTypes.AddTodo,
                    Value = new Todo
                    {
                        Id = nextTodoId++,
                        Text = iVal
                    }
                }
                );
                iVal = "";
            })">
        Click me
    </button>
    <ul>
        @if (StoreManager.Store.State != null && visibleTodos != null)
        {
            @foreach (Todo t in visibleTodos)
            {
                <li accesskey="@t.Id" style="text-decoration:@(t.IsCompleted ? " line-through" : "none" )">
                    <div onclick="@(() =>
                                               {
                                                   StoreManager.Store.Dispatch(new TodoAction
                                                   {
                                                       Type = ActionTypes.ToggleTodo,
                                                       Value = new Todo { Id = t.Id }
                                                   });

                                               })">
                        @t.Text
                    </div>
                </li>
            }
        }
    </ul>

    <div><FilterLink Filter="@FilterTodoTypes.ShowAll">All</FilterLink>, <FilterLink Filter="@FilterTodoTypes.ShowInProgress">In progress</FilterLink>, <FilterLink Filter="@FilterTodoTypes.ShowCompleted">Completed</FilterLink></div>
    <br />
    <button onclick="@(() => StoreManager.Store.Subscribe(Render))">
        Subscribe
    </button>
    <button onclick="@(() => StoreManager.Store.Subscribe(Render).Result())">
        Unsubscribe
    </button>

}
@functions {
// Component part
string iVal = "task 0";
int nextTodoId;

public Todo[] GetVisibleTodos(Todo[] allTodos, FilterTodoTypes filter)
{
    switch (filter)
    {
        case FilterTodoTypes.ShowCompleted:
            return allTodos.Where(t => t.IsCompleted).ToArray();

        case FilterTodoTypes.ShowInProgress:
            return allTodos.Where(t => !t.IsCompleted).ToArray();

        case FilterTodoTypes.ShowAll:
        default:
            return allTodos;
    }
}

Todo[] visibleTodos;

protected override async Task OnInitAsync()
{
    await StoreManager.Store.Subscribe(Render);
}

public void Render()
{
    visibleTodos = GetVisibleTodos(StoreManager.Store.State.Todos, StoreManager.Store.State.VisibilityFilter);
    this.StateHasChanged();
}

}

Et voilà le résultat :



Conclusion

Dans cet article, j'ai implémenté un exemple simple de liste de choses à faire avec Redux en Blazor. Il ne vous reste plus qu'à intégrer ça dans vos projets pour bien vous rendre compte de l'intérêt de cette solution.


La solution avec les exemples de compteur et de la liste Todo sont disponibles sur https://github.com/ToovR/Bladux.

Commentaires :

Aucun commentaires pour le moment


Laissez un commentaire :

Réalisé par
Expaceo