Динамические компоненты Blazor (DynamicComponent)

Одним из нововведений в Blazor .NET 6 стала возможность использовать динамические компоненты (DynamicComponent). Стандартный подход к созданию пользовательского интерфейса в Blazor состоит в том, чтобы разбить интерфейс на части (компоненты) и, в зависимости от потребностей пользователя составлять из этих компонентов всё содержимое той или иной страницы. DynamicComponent расширяет возможности по построению пользовательского интерфейса в Blazor.

Пример использования компонентов в .NET 5

Допустим, нам необходимо вывести приветствие пользователей на главной странице приложения. В .NET 5 мы бы создали такой компонент Greetimg.razor:

<p>Привет, <b>@Name</b></p>

@code {
    [Parameter]
    public string Name{ get; set; }
}

И, соответственно, использовали бы его на главной странице приложения:

@page "/"

<PageTitle>Index</PageTitle>

<Greeting Name="Вася"/>
<Greeting Name="Петя" />
<Greeting Name="Вова" />

Такой подход к построению интерфейса отлично подходит для тех случаев, когда мы заранее знаем, какие именно компоненты необходимо отобразить пользователю. Но могут быть и такие случаи, когда нам необходимо отображать различные компоненты во время выполнения на основе каких-либо данных. Например, мы хотим создать систему мониторинга чего-либо и пользователь может выбрать то, какие виджеты ему необходимо показывать, а какие скрыть. В этом случае нам необходимо каким либо образом сохранить состояние (данные о выбранных пользователем виджетах) и при загрузке приложения показать пользователю именно то, что он выбрал. В таком случае нам и пригодится нововведение .NET 6 — использование динамических компонентов в Blazor.

DynamicComponent в .NET 6

Рассмотрим процесс создания и использования динамических компонентов в Blazor на примере той же систему мониторинга. Пусть в нашей системе будут использоваться  компоненты Counter и FetchData из примера приложения Blazor Server и компонент для отсчёта времени (назовем его Chronometer)

@implements IDisposable

<p>@DateTimeCounter</p>

@code {
    [Parameter]
    public DateTime DateTimeCounter { get; set; } = DateTime.Now;

    private bool calc = false;

    protected async Task GetDateTime()
    {
        while (calc)
        {
            DateTimeCounter = DateTime.Now;
            await Task.Delay(1000);
            await InvokeAsync(StateHasChanged);
        }
    }

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            calc = true;
            await GetDateTime();
        }
    }

    public void Dispose()
    {
        calc = false;
    }
}

В зависимости от выбора пользователя мы должны показывать выбранные компоненты. Для начала создадим в нашем приложении класс модели:

using Dynamic.Pages;
using Dynamic.Shared;

namespace Dynamic.Models
{
    public class WidgetModel
    {
        private bool showCounter;

        public bool ShowCounter { 
            get 
            { 
                return showCounter; 
            }
            set
            {
                showCounter = value;
                if (showCounter)
                {
                    if (Widgets.Contains(typeof(Counter)))
                        return;
                    else
                        Widgets.Add(typeof(Counter));
                }
                else
                {
                    Widgets.Remove(typeof(Counter));
                };
            }
        }
        private bool showFetchData;

        public bool ShowFetchData {
            get 
            { 
                return showFetchData; 
            }
            set 
            { 
                showFetchData = value;
                if (showFetchData)
                {
                    if (Widgets.Contains(typeof(FetchData)))
                        return;
                    else
                        Widgets.Add(typeof(FetchData));
                }
                else
                {
                    Widgets.Remove(typeof(FetchData));
                };
            } 
        }
        private bool showChronometer;

        public bool ShowChronometer
        {
            get
            {
                return showChronometer;
            }
            set
            {
                showChronometer = value;
                if (showChronometer)
                {
                    if (Widgets.Contains(typeof(Chronometer)))
                        return;
                    else
                        Widgets.Add(typeof(Chronometer));
                }
                else
                {
                    Widgets.Remove(typeof(Chronometer));
                };
            }
        }

        public List<Type> Widgets { get; private set; }

        public WidgetModel()
        {
            Widgets = new();
        }
    }
}

В классе WidgetModel формируется список типов отображаемых виджетов на главной странице List<Type>. Теперь на главной странице приложения создадим форму и привяжем свойства модели к трем элементам InputCheckBox:

@page "/"

@using Dynamic.Models;

<PageTitle>Index</PageTitle>

<EditForm Model="widgetModel">
    <InputCheckbox @bind-Value="widgetModel.ShowCounter" id="counter"></InputCheckbox>
    <label for="counter">Счётчик</label>
    <InputCheckbox @bind-Value="widgetModel.ShowFetchData" id="fetchData"></InputCheckbox>
    <label for="fetchData">Погода</label>
    <InputCheckbox @bind-Value="widgetModel.ShowChronometer" id="chronometer"></InputCheckbox>
    <label for="chronometer">Хронометр</label>
</EditForm>

@code 
{
    WidgetModel widgetModel = new();
}

Теперь наша модель будет формировать список отображаемых виджетов. Чтобы отобразить все выбранные виджеты воспользуется динамическим компонентом (DynamicComponent)Blazor следующим образом:

@page "/"

@using Dynamic.Models;

.......

@foreach (var widgetType in widgetModel.Widgets)
{
    <DynamicComponent Type="@widgetType" />
}

@code 
{
    WidgetModel widgetModel = new();

}

Запустим приложение и убедимся, что наши виджеты отображаются или скрываются по мере их выбора пользователем:

Такая работа приложения с DynamicComponent прекрасно подходит для таких простых компонентов, как Counter и FetchData. Однако, что делать, если нам помимо, собственно, создания компонента необходимо будет также передать ему и набор параметров? DynamicComponent поддерживает такую возможность.

Передача параметров в DynamicComponent

Пусть наш компонент Chronometer принимает в качестве параметра название виджета. Допишем компонент следующим образом:

@implements IDisposable

<h1>@Name</h1>

<p>@DateTimeCounter</p>

@code {
    [Parameter]
    public DateTime DateTimeCounter { get; set; } = DateTime.Now;

    [Parameter]
    public string Name{ get; set; }


    private bool calc = false;

    protected async Task GetDateTime()
    {
        while (calc)
        {
            DateTimeCounter = DateTime.Now;
            await Task.Delay(1000);
            await InvokeAsync(StateHasChanged);
        }
    }

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            calc = true;
            await GetDateTime();
        }
    }

    public void Dispose()
    {
        calc = false;
    }
}

Теперь допишем нашу модель таким образом, чтобы список Widgets содержал не только тип создаваемого компонента, то и его параметры. Для этого создадим класс виджета:

namespace Dynamic.Models
{
    public class Widget
    {
        public Type Type { get; set; }
        public IDictionary<string, object> Parameters;

        public Widget(Type type)
        {
            Type = type;
            Parameters = new Dictionary<string, object>();
        }
    }
}

Все параметры компонента будем хранить в виде словаря Dictionary<string, object>. Теперь доработаем модель:

using Dynamic.Pages;
using Dynamic.Shared;

namespace Dynamic.Models
{
    public class WidgetModel
    {
        private bool showCounter;

        public bool ShowCounter { 
            get 
            { 
                return showCounter; 
            }
            set
            {
                showCounter = value;
                if (showCounter)
                {
                    if (Widgets.Contains(new Widget(typeof(Counter))))
                        return;
                    else
                        Widgets.Add(new Widget(typeof(Counter)));
                }
                else
                {
                    Widgets = Widgets.Where(w=>(w.Type != typeof(Counter))).ToList();
                };
            }
        }
        private bool showFetchData;

        public bool ShowFetchData {
            get 
            { 
                return showFetchData; 
            }
            set 
            { 
                showFetchData = value;
                if (showFetchData)
                {
                    if (Widgets.Contains(new Widget(typeof(FetchData))))
                        return;
                    else
                        Widgets.Add(new Widget(typeof(FetchData)));
                }
                else
                {
                    Widgets = Widgets.Where(w => (w.Type != typeof(FetchData))).ToList();
                };
            } 
        }
        private bool showChronometer;

        public bool ShowChronometer
        {
            get
            {
                return showChronometer;
            }
            set
            {
                showChronometer = value;
                if (showChronometer)
                {
                    if (Widgets.Contains(new Widget(typeof(Chronometer))))
                        return;
                    else
                    {
                        Widget widget = new(typeof(Chronometer));
                        widget.Parameters.Add("Name","Хронометр");
                        Widgets.Add(widget);
                    }
                }
                else
                {
                    Widgets = Widgets.Where(w => (w.Type != typeof(Chronometer))).ToList();
                };
            }
        }

        public List<Widget> Widgets { get; private set; }

        public WidgetModel()
        {
            Widgets = new();
        }
    }
}

В качестве примера, я переписал set у свойства ShowChronometer и прямо в коде модели добавляю в параметры объекта Widget название компонента. При этом, никто не запрещает вам изменять содержимое словаря в любом удобном для вам месте в приложении (например, можно дать возможность пользователю самому задавать название виджета). Теперь изменим наш цикл foreach на главной странице приложения следующим образом:

@foreach (var widgetType in widgetModel.Widgets)
{
    <DynamicComponent Type="@widgetType.Type" Parameters="@widgetType.Parameters" />
}

Здесь во втором параметре у DynamicComponent  мы как раз и передаем наш словарь с параметрами компонента. Теперь можно запустить приложение, выбрать показ компонента Chronometer и убедиться, что значение свойства Name было присвоено компоненту:

Теперь всё работает так как и задумано и, более того, наши динамические компоненты принимают также параметры. Ну, а для того, чтобы сохранить выбор пользователя, Вы можете использовать самые различные механизмы. Например, воспользоваться хранилищем браузера.

Итого

DynamicComponent в Blazor .NET 6 позволяет динамически создавать любые компоненты в процессе выполнения приложения. Для создания динамического компонента Blazor нам необходимо передать в DynamicComponent тип создаваемого компонента и, при необходимости, список параметров в виде словаря Dictionary<string, object>.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии