ASP.NET Core Blazor Webassembly 之 组件

原文:ASP.NET Core Blazor Webassembly 之 组件

关于组件

现在前端几大*全面组件化。组件让我们可以对常用的功能进行封装,以便复用。组件这东西对于搞.NET的同学其实并不陌生,以前ASP.NET WebForm的用户控件其实也是一种组件。它封装html代码,封装业务逻辑,对外提供属性事件等信息,它完完全全就是个组件,只是用户控件跑在服务端,而现在的组件大多数直接跑在前端。现在Blazor Webassembly微软正式把组件带到前端,让我们看看它是怎么玩的。

第一个组件

废话不多说下面开始构建第一个组件。这个组件很简单就是绿色的面板加一个标题的容器,我们就叫它GreenPanel吧。

新建Blazor Webassembly项目

前几天的build大会,Blazor Webassembly已经正式release了。我们更新最新版的Core SDK就会安装正式版的模板。
ASP.NET Core Blazor Webassembly 之 组件
新建项目选Blazor Webassembly App项目模板

新建GreenPanel组件

在pages命令下新建一个文件夹叫做components,在文件夹下新建一个razor组件,命名为GreenPanel.razor。

注意:组件的命名必须大写字母开头

ASP.NET Core Blazor Webassembly 之 组件
添加代码如下:

<div class="green-panel">
    <div class="title">
        Green panel
    </div>
    <div class="content">
    </div>
</div>

<style>
    .green-panel{
        background-color: green;
        height:400px;
        width:400px;
    }
    .green-panel .title {
        border-bottom:1px solid #333;
        height:30px;
    }
    .green-panel .content {
    }
</style>

@code { override void OnInitialized()
        {
            base.OnInitialized();
        }
}

一个组件主要是由html,style ,code等组成。html,style用来控制ui表现层,code用来封装逻辑。

注意:Blazor目前没有样式隔离技术,所以写在组件内的style有可能会影响其他html元素

使用组件

使用组件跟其他框架大体是相同的,直接在需要使用的地方使用以我们组件名作为一个html元素插入:
如果不在同一层目录下,则需要导入命名空间。在_Imports.razor文件内引用组件的命名空间:

...
@using BlazorWasmComponent.Components

在index页面使用组件:

<GreenPanel></GreenPanel>

运行一下:
ASP.NET Core Blazor Webassembly 之 组件

组件类

每个组件最后都会编译成一个C#类,让我们用ILSPy看看一眼长啥样:

// BlazorWasmComponent.Components.GreenPanel
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;

public class GreenPanel : ComponentBase
{
	protected override void BuildRenderTree(RenderTreeBuilder __builder)
	{
		__builder.AddMarkupContent(0, "<div class=\"green-panel\">\r\n    <div class=\"title\">\r\n        Green panel\r\n    </div>\r\n    <div class=\"content\">\r\n    </div>\r\n</div>\r\n\r\n");
		__builder.AddMarkupContent(1, "<style>\r\n    .green-panel{\r\n        background-color: green;\r\n        height:400px;\r\n        width:400px;\r\n    }\r\n    .green-panel .title {\r\n        border-bottom:1px solid #333;\r\n        height:30px;\r\n    }\r\n    .green-panel .content {\r\n    }\r\n</style>");
	}

	protected override void OnInitialized()
	{
		base.OnInitialized();
	}
}

GreenPanel组件会编译成一个GreenPanel类,继承自ComponentBase基类。里面有几个方法:

  1. BuildRenderTree 用来构建html,css等ui元素
  2. 其它code部分会也会被合并到这个类里面

生命周期

了解组件声明周期对我们使用组件有很大的帮助。一个组件的声周期主要依次以下几个阶段:

  1. OnInitialized、OnInitializedAsync
  2. OnParametersSet、OnParametersSetAsync
  3. OnAfterRender、OnAfterRenderAsync
  4. Dispose

如果要在每个生命阶段插入特定的逻辑,请重写这些方法:

@implements IDisposable

@code {
    protected override void OnInitialized()
    {
        Console.WriteLine("OnInitialized");
        base.OnInitialized();
    }
    protected override Task OnInitializedAsync()
    {
        Console.WriteLine("OnInitializedAsync");
        return base.OnInitializedAsync();
    }
    protected override void OnParametersSet()
    {
        Console.WriteLine("OnParametersSet");
        base.OnParametersSet();
    }
    protected override Task OnParametersSetAsync()
    {
        Console.WriteLine("OnParametersSetAsync");

        return base.OnParametersSetAsync();
    }
    protected override void OnAfterRender(bool firstRender)
    {
        Console.WriteLine("OnAfterRender");
        base.OnAfterRender(firstRender);
    }
    protected override Task OnAfterRenderAsync(bool firstRender)
    {
        Console.WriteLine("OnAfterRenderAsync");
        return base.OnAfterRenderAsync(firstRender);
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose");
    }
}

注意:组件默认并不继承IDisposable接口,如果要重写Dispose方法请手工使用@implements方法继承接口IDisposable

运行一下,并且切换一下页面,使组件销毁,可以看到所有生命周期方法依次执行:
ASP.NET Core Blazor Webassembly 之 组件

组件属性

我们定义组件总是免不了跟外部进行交互,比如从父组件接受参数,或者把自身的数据对外暴露。我们可以使用[Parameter]来定义一个组件的属性。这里叫做Parameter,估计是为了跟C#里的属性(property,attribute)进行区分。
对我们的GreenPanel组件进行改进,支持从外部定义标题的内容:

<div class="green-panel">
    <div class="title">
        @Title
    </div>
    <div class="content">
    </div>
</div>

<style>
    .green-panel {
        background-color: green;
        height: 400px;
        width: 400px;
    }

        .green-panel .title {
            border-bottom: 1px solid #333;
            height: 30px;
        }

        .green-panel .content {
        }
</style>

@code {

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

    protected override void OnInitialized()
    {
        base.OnInitialized();
    }

}

在父组件使用:

@page "/"

<GreenPanel Title="Panel A"></GreenPanel>

运行一下:
ASP.NET Core Blazor Webassembly 之 组件

上面传递的是简单类型String,下面让我们试试传递复杂类型的数据进去。我们继续对GreenPanel改造。改造成ColorPanel,它接受一个Setting对象来设置标题跟背景颜色。
定义Setting类:

  public class PanelSetting
    {
        public string Title { get; set; }

        public string BgColor { get; set; }
    }

定义ColorPanel:

<div class="green-panel">
    <div class="title">
        @Setting.Title
    </div>
    <div class="content">
    </div>
</div>

<style>
    .green-panel {
        background-color: @Setting.BgColor;
        height: 400px;
        width: 400px;
    }

        .green-panel .title {
            border-bottom: 1px solid #333;
            height: 30px;
        }

        .green-panel .content {
        }
</style>

@using BlazorWasmComponent.models;
@code {

    [Parameter]
    public PanelSetting Setting { get; set; }

    protected override void OnInitialized()
    {
        base.OnInitialized();
    }

}

在父组件使用:

@page "/"

<p>@PanelSetting.Title</p>
<p>@PanelSetting.BgColor</p>

<ColorPanel Setting="PanelSetting"></ColorPanel>

@using BlazorWasmComponent.models;
@code{  

    public PanelSetting PanelSetting { get; set; }

    protected override void OnInitialized()
    {
        PanelSetting = new PanelSetting
        {
            BgColor = "Red",
            Title = "Panel RED"
        };

        base.OnInitialized();
    }
}

运行一下:
ASP.NET Core Blazor Webassembly 之 组件

注意:上一篇WebAssembly初探里有个错误,当时认为这个属性是单向数据流,经过试验子组件对父组件传入的数据源进行修改的时候其实是会反应到父组件的,只是如果你使用@符号绑定数据的时候并不会像angularjs,vue等立马进行刷新。关于这个事情感觉可以单独写一篇,这里就不细说了。

组件事件

我们的组件当然也可以提供事件,已供外部订阅,然后从内部激发来通知外部完成业务逻辑,实现类似观察者模式。继续改造ColorPanel,当点击时候对外抛出事件。
使用EventCallback、EventCallback< T > 来定义事件:

<div class="green-panel" @onclick="DoClick">
    <div class="title">
        @Setting.Title
    </div>
    <div class="content">
    </div>
</div>

<style>
    .green-panel {
        background-color: @Setting.BgColor;
        height: 400px;
        width: 400px;
    }

        .green-panel .title {
            border-bottom: 1px solid #333;
            height: 30px;
        }

        .green-panel .content {
        }
</style>

@using BlazorWasmComponent.models;
@code {

    [Parameter]
    public PanelSetting Setting { get; set; }
    [Parameter]
    public EventCallback OnClick { get; set; }

    protected override void OnInitialized()
    {

        base.OnInitialized();
    }

    public void DoClick()
    {
        OnClick.InvokeAsync(null);
    }
}


父组件订阅事件:

@page "/"

<p>
    子组件点击次数:@ClickCount
</p>
<ColorPanel Setting="PanelSetting" OnClick="HandleClick"></ColorPanel>

@using BlazorWasmComponent.models;
@code{  

    public PanelSetting PanelSetting { get; set; }

    public int ClickCount { get; set; }

    protected override void OnInitialized()
    {
        PanelSetting = new PanelSetting
        {
            BgColor = "Red",
            Title = "Panel RED"
        };

        base.OnInitialized();
    }

    private void HandleClick()
    {
        ClickCount++;
    }
}

运行一下,并点击子组件,父组件的计数器会被+1:
ASP.NET Core Blazor Webassembly 之 组件

子内容

当我们定义容器级别的组件时往往需要往组件内传递子内容。比如我们的ColorPanel明显就有这种需求,这个Panel内部会被放上其它元素或者其它组件,这个时候我们可以使用ChildContent属性来实现。

<div class="green-panel" @onclick="DoClick">
    <div class="title">
        @Setting.Title
    </div>
    <div class="content">
        @ChildContent
    </div>
</div>

<style>
    .green-panel {
        background-color: @Setting.BgColor;
        height: 400px;
        width: 400px;
    }

        .green-panel .title {
            border-bottom: 1px solid #333;
            height: 30px;
        }

        .green-panel .content {
        }
</style>

@using BlazorWasmComponent.models;
@code {

    [Parameter]
    public PanelSetting Setting { get; set; }
    [Parameter]
    public EventCallback OnClick { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    protected override void OnInitialized()
    {

        base.OnInitialized();
    }

    public void DoClick()
    {
        OnClick.InvokeAsync(null);
    }
}

定义一个类型为RenderFragment名称为ChildContent的属性,然后在html内使用@ChildContent来指代它。这样子内容就会被替换到指定的位置。
父组件使用,我们给ColorPanel的内部设置一个文本框吧:

@page "/"

<p>
    子组件点击次数:@ClickCount
</p>
<ColorPanel Setting="PanelSetting" OnClick="HandleClick">

   输入框: <input />

</ColorPanel>

@using BlazorWasmComponent.models;
@code{  

    public PanelSetting PanelSetting { get; set; }

    public int ClickCount { get; set; }

    protected override void OnInitialized()
    {
        PanelSetting = new PanelSetting
        {
            BgColor = "Red",
            Title = "Panel RED"
        };

        base.OnInitialized();
    }

    private void HandleClick()
    {
        ClickCount++;
    }
}

运行一下看看我们的文本框会不会出现在panel内部:
ASP.NET Core Blazor Webassembly 之 组件

@ref

因为我们的组件使用是在html内,当你在@code内想要直接通过代码操作子组件的时候可以给子组件设置@ref属性来直接获取到子组件的对象。继续改造ColorPanel,在它初始化的时候生产一个ID。

<div class="green-panel" @onclick="DoClick">
    <div class="title">
        @Setting.Title
    </div>
    <div class="content">
        @ChildContent
    </div>
</div>

<style>
    .green-panel {
        background-color: @Setting.BgColor;
        height: 400px;
        width: 400px;
    }

        .green-panel .title {
            border-bottom: 1px solid #333;
            height: 30px;
        }

        .green-panel .content {
        }
</style>

@using BlazorWasmComponent.models;
@code {

    public string ID { get; set; }

    [Parameter]
    public PanelSetting Setting { get; set; }
    [Parameter]
    public EventCallback OnClick { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    protected override void OnInitialized()
    {
        ID = Guid.NewGuid().ToString();
        base.OnInitialized();
    }

    public void DoClick()
    {
        OnClick.InvokeAsync(null);
    }
}

修改父组件,添加一个按钮,当点击的时候直接获取子组件的Id:

@page "/"

<p>
    子组件ID:@subId
</p>
<ColorPanel Setting="PanelSetting" OnClick="HandleClick" @ref="colorPanel">
   输入框: <input />
</ColorPanel>
<button @onclick="GetSubComponentId" class="btn btn-info">获取子组件ID</button>

@using BlazorWasmComponent.models;
@code{  

    private string subId;

    private ColorPanel colorPanel;

    public PanelSetting PanelSetting { get; set; }

    public int ClickCount { get; set; }

    protected override void OnInitialized()
    {
        PanelSetting = new PanelSetting
        {
            BgColor = "Red",
            Title = "Panel RED"
        };

        base.OnInitialized();
    }

    private void HandleClick()
    {
        ClickCount++;
    }

    private void GetSubComponentId ()
    {
        this.subId = colorPanel.ID;
    }
}

运行一下:
ASP.NET Core Blazor Webassembly 之 组件

@key

当使用循环渲染组件的时候请在组件上使用@key来加速Blazor的diff算法。有了key就可以快速的区分哪些组件是可以复用的,哪些是要新增或删除的,特别是在对循环列表插入对象或者删除对象的时候特别有用。如果使用过vue就应该很容易明白有了key可以降低虚拟dom算法的复杂度,在这里猜测blazor内部应该也是类似的算法。

@page "/"


@foreach (var key in List)
{
    <ColorPanel @key="key" Setting="PanelSetting"></ColorPanel>
}

@using BlazorWasmComponent.models;
@code{  

    public List<String> List = new List<string>
    {
        Guid.NewGuid().ToString(),
         Guid.NewGuid().ToString(),
          Guid.NewGuid().ToString()
    };

    public PanelSetting PanelSetting { get; set; }


    protected override void OnInitialized()
    {
        PanelSetting = new PanelSetting
        {
            BgColor = "Red",
            Title = "Panel RED"
        };

        base.OnInitialized();
    }

}

太晚了就这样吧,喜欢的话请点个赞,谢谢!

相关内容:
ASP.NET Core Blazor 初探之 Blazor WebAssembly
ASP.NET Core Blazor 初探之 Blazor Server

ASP.NET Core Blazor Webassembly 之 组件

上一篇:IOS 打包提示错误(ERROR ITMS-90125: ERROR ITMS-90087: ERROR ITMS-90209:)


下一篇:不用逗号进行UNION