• Blazor组件的new使用方式与动态弹窗


    1. 前言

    Blazor中的无状态组件文中,我提到了无状态组件中,有人提到这个没有diff,在渲染复杂model时,性能可能会更差。确实,这一点确实是会存在的。以上文的方式来实现无状态组件,确实只要属性发生变化,就会渲染。无状态组件是否渲染,更多的需要依靠父组件来判断。父组件不用更新,则无状态组件自然不会发生渲染。此外,有些需求,比如地图,要做的就是每次拖拽、缩放,整个地图中都要被渲染,这种纯粹用来进行数据展示的组件,使用无状态组件会更好。如果想要无状态组件不会每次都渲染,那就可以自己实现一个ShouldRender的函数。

    2. 一定要实现IComponent接口吗?

    Blazor中的无状态组件中,我提到一个组件要想被成功被编译使用,需要满足两个条件:

    1. 实现IComponent接口
    2. 具有一个如下声明的虚函数:protected virtual void BuildRenderTree(RenderTreeBuilder builder);

    那,如果我们把IComponent接口的实现声明给去掉(即仅删除: IComponent),能够使用吗?显然不能,VS编译器都会提示你错误找不到这个组件:

    RZ10012 Found markup element with unexpected name 'xx.DisplayCount'. If this is intended to be a component, add a @using directive for its namespace.

    但是再想一下,vs会把所有的*.razor文件编译为一个类,那我们不是可以直接使用这个类,new一个组件吗?这是当然是没问题的。

    3. 再谈Blazor组件的渲染

    Blazor中的无状态组件中,我谈到Blazor的渲染,实际上渲染的是组件内生成的RenderFragmentDOM树。当我们创建一个*.razor文件后,编译器会自动帮我们将组件中的DOM生成为RenderFragment。因此无论一个*.razor文件是否继承ComponmentBase类,异或是是否实现IComponent接口,只要满足上述的第二个条件——具有一个BuildRenderTree的虚函数——就一定能够将文件内所编辑的DOM转为RenderFragmentDOM树。在之前的文章中,无状态组件StatelessComponentBase基类的声明大致如下:

    public class StatelessComponentBase : IComponent
    {
        private RenderFragment _renderFragment;
    
        public StatelessComponentBase()
        {
            // 设置组件DOM树(的创建方式)
            _renderFragment = BuildRenderTree;
        }
        
        ...
    }
    

    说白了,无非是我们耍了个小聪明,利用编译器对*.razor的编译方式,自动生成了RenderFragment。可是,没人说_renderFragment一定是要私有的,我们完全可以这样:

    public class StatelessComponentBase 
    {
        public RenderFragment RenderFragment { get; private set; }
    
        public StatelessComponentBase()
        {
            RenderFragment = BuildRenderTree;
        }
        ...
    }
    

    这样子,我们就可以在组件外部获取到RenderFragmentDOM树。

    4. New一个组件

    在3中,我们已然将组件中的RenderFragment暴露到了外部,自然也就能够在new之后,通过实例来获取到它:

    new Componment().RenderFragment
    

    来看一个例子,是基于Counter页面修改的:

    // DisplayCount组件
    @inherits StatelessComponentBase
    
    <h3>DisplayCount</h3>
    <p role="status">Current count: @Count</p>
    
    
    @code {
        public int Count{ get; set; }
    }
    
    ==================
    
    // index页面
    @page "/"
    <PageTitle>Index</PageTitle>
    
    <div>currentCount: @currentCount</div>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
    
    @renderFragment
    
    @code {
        RenderFragment? renderFragment = null;
        private int currentCount = 1;
    
        Components.DisplayCount displayCount = new Components.DisplayCount();
        private void IncrementCount()
        {
            currentCount++;
            displayCount.Count = currentCount;
        }
    
    
        public async override Task SetParametersAsync(ParameterView parameters)
        {
            displayCount.Count = currentCount;
            renderFragment = displayCount.RenderFragment;
            await base.SetParametersAsync(parameters);
        }
    }
    

    代码运行无任何问题:

    通过这种小技巧,即可实现Blazor的动态渲染。提到Blazor的动态渲染,有些人可能会讲到DynamicComponentDynamicComponent通过接收组件的类型——Type,和组件的参数——Parameters,能够实现对组件进行动态渲染。这里提供了一个与DynamicComponent不同的思路。DynamicComponent需要将标签写在组件文件中,以实现挂载,而本文则是通过new一个组件来获取内部的RenderFragment来进行。插个题外话,DynamicComponent也没有继承ComponmentBase类,和之前我提出的无状态组件的结构是相似的。

    5. 动态弹窗1

    想一想在使用WinForm的时候,我们只需要new和show一下,就可以打开一个窗体。现在有空动态渲染,Blazor中的弹窗组件,new and show不再是梦想。

    以上述方法而言,new一个组件后,必然需要在一个组件中进行挂载。没有挂载点,Blazor组件是无法在页面上呈现出来的。一想到挂载点,我们自然会想到创建一个全局容器来实现。当我们调用show的时候,在容器组件中将RenderFragment进行渲染即可。

    Modal.razor:Modal的基类,为其添加了show和close的方法。因为Modal组件都是new出来的,需要挂载在Blazor中才能够渲染,这里通过Container中的静态属性ModalContainer来进行Modal的渲染。

    @inherits StatelessComponentBase
    
    @code {
    
        public void Show()
        {
            Container.ModalContainer?.AddModal(this);
        }
    
        public void Close()
        {
            Container.ModalContainer?.RemoveModal(this);
        }
    }
    

    Container.razor:全局容器,用于挂载Modal。

    <div style="position: absolute; top:0;  100%; height: 100%; z-index: 9999; pointer-events: none;
                display: flex; align-items:center; justify-content:center;">
        @foreach(var modal in Modals)
        {
            <div @key="modal" style="border: 1px solid #efefef; border-radius: 4px;">
                @modal.RenderFragment
            </div>
        }
    </div>
    
    @code {
        /// <summary>
        /// 由于需要在每个 new 的 Modal 中能够获取到container的实例,
        ///  所以这里需要用个静态变量,在组件初始化的时候引用自身
        /// </summary>
        internal static Container? ModalContainer { get; private set; }
    
    
        private List<Modal> Modals { get; init; } = new List<Modal>();
    
        protected override Task OnInitializedAsync()
        {
            ModalContainer = this;
            return base.OnInitializedAsync();
        }
    
        internal void AddModal(Modal modal)
        {
            if (!Modals.Contains(modal))
            {
                Modals.Add(modal);
                StateHasChanged();
            }
        }
    
        internal void RemoveModal(Modal modal)
        {
            if (Modals.Contains(modal))
            {
                Modals.Remove(modal);
                StateHasChanged();
            }
        }
    
    }
    

    接下来,当我们需要添加一个Modal组件的时候,我们只需要继承Modal即可。

    // Modal1.razor
    @inherits Modal
    
    <h3>Modal1</h3>
    

    而在使用的时候,我们可以new and show即可:

    @page "/modalDemo"
    @using Instantiation.Components.DyModal1
    
    
    <PageTitle>Modal Demo</PageTitle>
    
    <button class="btn btn-primary" @onclick="()=>{modal1.Show();}">OpenModal1</button>
    <button class="btn btn-primary" @onclick="()=>{modal1.Close();}">CloseModal1</button>
    
    @code {
        Modal modal1 = new Modal1();
    }
    

    效果如下:

    注意最后的部分,Modal2关闭再打开后,count的计数值是没有变化的,也就是说这种方式可以保留Modal内部的状态。但是,Modal中子组件与Modal根组件(例如Modal1.razor)有些交互无法使用,例如**EventCallback** 等。

    这部分的代码详见:[Pages/ModalDemo.razor](BlazorTricks/ModalDemo2.razor at main · zxyao145/BlazorTricks (github.com))。

    6. 动态弹窗2

    当然,使用DynamicComponent也是可以实现的,这样你就不必使用new and show,可以直接使用泛型Add<T>()来实现,而且可以直接使用ComponentBase,不必将RenderFragment暴露出来。

    Container2.razor:同样需要定义一个全局的容器:

    
    <div style="position: absolute; top:0;  100%; height: 100%; z-index: 9999; pointer-events: none;
                display: flex; align-items:center; justify-content:center;">
        @foreach(var modal in Modals)
        {
            <div @key="modal" style="border: 1px solid #efefef; border-radius: 4px; pointer-events: all;">
                <DynamicComponent Type="modal" />
            </div>
        }
    </div>
    
    @code {
        /// <summary>
        /// 由于需要在每个 new 的 Modal 中能够获取到container的实例,
        ///  所以这里需要用个静态变量,在组件初始化的时候引用自身
        /// </summary>
        internal static Container2? ModalContainer { get; private set; }
    
    
        private List<Type> Modals { get; init; } = new List<Type>();
    
        protected override Task OnInitializedAsync()
        {
            ModalContainer = this;
            return base.OnInitializedAsync();
        }
    
        internal void AddModal<T>()
        {
            var type = typeof(T);
            if (!Modals.Contains(type))
            {
                Modals.Add(type);
                StateHasChanged();
            }
        }
    
        internal void RemoveModal<T>()
        {
            var type = typeof(T);
            if (Modals.Contains(type))
            {
                Modals.Remove(type);
                StateHasChanged();
            }
        }
    }
    

    Modal3.razor:具体的弹窗Modal,注意,不必写任何继承

    <h3>Modal3</h3>
    
    @currentCount
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
    
    @code {
        private int currentCount = 0;
    
        private void IncrementCount()
        {
            currentCount++;
            StateHasChanged();
        }
    }
    

    使用:

    @page "/modalDemo2"
    @using Instantiation.Components.DyModal2
    
    <PageTitle>Modal Demo2</PageTitle>
    
    <button class="btn btn-primary" @onclick="()=>{Container2.ModalContainer?.AddModal<Modal3>();}">Open DynamicComponentModalIns</button>
    <button class="btn btn-primary" @onclick="()=>{Container2.ModalContainer?.RemoveModal<Modal3>();}">Close DynamicComponentModalIns</button>
    

    效果如下:

    注意,这种方式Modal关闭再打开后,count的计数值是重新归位了0,也就是说这种方式无法保留Modal内部的状态

    这部分的代码详见:[Pages/ModalDemo2.razor](BlazorTricks/ModalDemo2.razor at main · zxyao145/BlazorTricks (github.com))。

    7. 总结

    本文讲述Blazor如何通过new实例化的方式进行使用,继而引起了动态弹窗的使用。动态弹窗本文写了两种方式,一是之前提到的new and show,需要较多的编码,另外一种是利用Blazor内置的组件DynamicComponent。两种方式各有缺点。第一种可以保留内部的状态,但是Modal的跟组件与子组件的交互将有部分功能缺失(不限制于Modal的子孙组件);第二种可以可以保留组件的一切功能,但是Modal在关闭后再次打开无法保留之前内部的状态(其实这部分是可以解决的,提示:DynamicComponentRender方法)。

    示例代码:BlazorTricks/02-Instantiation at main · zxyao145/BlazorTricks (github.com)

  • 相关阅读:
    数据中心 CLOS 架构
    CLOS网络的无阻塞条件
    网络层 IP 协议首部格式与其配套使用的四个协议(ARP,RARP,ICMP,IGMP)
    Redis数据库之经典考核习题
    Redis数据库之服务器主从配置
    Redis数据库之KEY的操作与事务管理
    Redis数据库之数据基本管理操作
    Redis数据库安装与配置调试
    基于windows的Redis后台服务安装卸载管理
    面向对象数据模型的构建和分析
  • 原文地址:https://www.cnblogs.com/zxyao/p/15756359.html
Copyright © 2020-2023  润新知