最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 《进击吧!Blazor!》第一章 5.组件开发

    正文概述 掘金(MicrosoftReactor)   2021-02-26   583

    这次分享我么要聊聊Blazor的精髓,也是我个人认为Blazor框架体系中最优秀的特性——组件。

    组件

    组件(Component)是对数据和方法的简单封装。几乎所有UI相关的框架都有组件(控件)的概念。

    《进击吧!Blazor!》第一章 5.组件开发 早期的Delphi组件叫做VCL(Visual Component Library),它采用自身嵌套的方式组合成所需的用户界面,并提供属性,方法,事件与组件外部进行交互,自身有着独立的生命周期,在必要的时候进行销毁。

    之后.Net的WinForms和WPF组件相对于Delphi虽然设计实现上完全不同,但是对组件的定义和用途上几乎一致。

    现在Web前端框架Angular中也采用了组件的概念,整体理念依旧相似。

    纵观这些框架的组件设计,可以提炼出组件包含以下特性。 《进击吧!Blazor!》第一章 5.组件开发 Blazor应用也是使用组件构建的。组件是自包含的用户界面 (UI) 块,例如页、对话框或窗体。 组件包含插入数据或响应 UI 事件所需的 HTML 标记和处理逻辑。 组件非常灵活且轻量。 可在项目之间嵌套、重复使用和共享。

    1.参数(属性)

    提供组件外部向组件内部传递数据的方式。

    在Blazor中我们称组件的属性(Property)叫参数(Parameter),参数本身就是一个属性,但是为了让Blazor框架能区分两者,所以我们在属性上增加 [Parameter]特性来声明属性为组件的参数。

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

    组件参数

    组件参数可以接收来在razor页面中给与的值,支持简单类型,也可以支持复杂类型。

    <!--组件代码-->
    <h1>Blazor is @Text!</h1>
    @code {
        [Parameter]
        public string Text { get; set; }
    }
    
    <!--组件使用-->
    <Component Title="Superior">
    

    上例就是将Superior通过参数传入组件,组件中就会输出Blazor is Superior!

    路由参数

    组件可以接收来自 @page 指令所提供的路由模板的路由参数。 路由器使用路由参数来填充相应的组件参数。参数类型受限于路由规则,只支持几个基本类型。

    <!--页面代码-->
    @page "/RouteParameter/{text}"
    <h1>Blazor is @Text!</h1>
    @code {
        [Parameter]
        public string Text { get; set; }
    }
    

    当使用/RouteParameter/Superior地址进行路由时,跳转到上例中的页面,并且页面输出Blazor is Superior!

    级联参数

    在某些情况下,使用组件参数将数据从祖先组件流向子代组件不太方便,尤其是在有多个组件层时。 级联值和参数提供了一种方便的方法,使祖先组件为其所有子代组件提供值,从而解决了此问题。

    祖先组件中使用CascadingValue设定需要向下传递的级联值,子代组件中使用 [CascadingParameter] 特性来声明级联参数用于接收级联值。

    本文后续会有详细的Demo来讲解此特性,此处暂不展开了。

    2.事件

    事件是一种由组件内部发起,由组件外部处理的一种机制。

    对于原始的Html元素与Razor组件在事件的使用上有一些细微差别,下面分开介绍。

    Html 元素

    对HTML 元素的事件采用@on{EVENT}格式(例如 @onclick)处理事件,Razor 组件将此属性的值视为事件处理程序。

    <h1>Blazor is @Text!</h1>
    <button @onclick="OnClick">Button</button>
    @code
    {
        private string Text { get; set; }
        void OnClick(MouseEventArgs e)
        {
            Text = "Superior";
        }
    }
    

    点击Button按钮后就触发@onclick事件,然后设置Text的值,最后组件输出Blazor is Superior! 每一个事件都会返回一个参数,@onclick事件返回MouseEventArgs参数,更多详见事件参数类型

    Razor 组件

    跨组件公开事件,可以使用 EventCallback。父组件可向子组件的 EventCallback 分配回调方法,由子组件完成调用。

    <!--子组件-->
    <button @onclick="OnBtnClick">Button</button>
    @code {
        [Parameter]
        public EventCallback<string> OnClick { get; set; }
    
        void OnBtnClick(MouseEventArgs e)
        {
            if (OnClick.HasDelegate)
                OnClick.InvokeAsync("Superior");
        }
    }
    
    <!--父组件-->
    <h1>Blazor is @Text!</h1>
    <Component OnClick="OnClick"></Component>
    @code
    {
        private string Text { get; set; }
        void OnClick(string e)
        {
            Text = e;
        }
    }
    

    《进击吧!Blazor!》第一章 5.组件开发

    EventCallback<string> OnClick 定义了一个名为OnClick的事件,EventCallback的泛型参数就是事件的参数类型。 OnClick.InvokeAsync("Superior") 调用这个事件,让注册的方法执行,注意事件调用前通过OnClick.HasDelegate判断事件是否有被注册,如果没有任何方法注册此事件,那么调用会发生异常。 OnClick="OnClick"OnClick方法注册给事件。

    3.方法

    组件对外暴露的方法,提供外部组件调用。

    <!--组件代码-->
    <h1>Blazor is @Text!</h1>
    @code
    { 
        private string Text { get; set; }
        public void SetText(string text)
        {
            Text = text;
            StateHasChanged();
        } 
    }
    
    <!--组件使用-->
    <Component @ref="@component"></Component>
    <button @onclick="OnClick">Button</button>
    @code
    {
        private Component component;
        void OnClick(MouseEventArgs e)
        {
            component.SetText("Superior");
        }
    }
    

    当点击Button按钮触发@onclick事件,通过Component组件的SetText方法设置组件的Text值,组件就输出Blazor is Superior! @ref 想要获得某个组件的实例,可以使用@ref特性,在这里他会把Component组件的实例填充到component变量中。此处注意,@ref的应用只有在组件完成呈现后才完成。

    4.数据绑定

    参数只提供了外部组件向组件单向赋值,数据绑定就是双向赋值。

    对于原始的Html元素与Razor组件在数据绑定的使用上有一些细微差别,下面分开介绍。

    Html 元素

    使用通过名为 @bind 的 Html 元素特性提供了数据绑定功能。

    <h4>Blazor is @Text!</h4>
    <input @bind="Text" />
    @code
    {
        private string Text;
    }
    

    《进击吧!Blazor!》第一章 5.组件开发

    Text变量绑定到input组件,当input中完成输入且离开焦点后输出Blazor is Superior!

    如果我们想要输入时立即显示输入的内容,我们可以通过带有 event 参数的 @bind:event 属性将绑定指向 oninput 事件。

    <h4>Blazor is @Text!</h4>
    <input @bind="Text" @bind:event="oninput"/>
    @code
    {
        private string Text;
    }
    

    《进击吧!Blazor!》第一章 5.组件开发

    Html元素绑定实现原理 Html元素本身并不支持双向属性绑定机制,当我们使用@bind后,Blazor帮我们生成了value="@Text"实现向Html元素赋值,再生成@onchange事件实现Html元素向绑定变量赋值。

    <input value="@Text"
        @onchange="@((ChangeEventArgs __e) => Text = __e.Value.ToString())" />
    
    @code {
        private string Text { get; set; }
    }
    
    

    5.嵌套

    组件嵌套就是允许一个组件成为另一组件的容器,通过父与子的层层嵌套实现各种复杂的界面,在这过程中我们也能提炼出相似的组件,加以重复使用和共享。

    下面是“我的一天”界面的代码以及他们组件的嵌套结构 《进击吧!Blazor!》第一章 5.组件开发

    子内容

    组件可以设置自己的某一个位置插入其他组件的内容。

    <!--组件代码-->
    <h1>Blazor is @ChildContent</h1>
    @code{
        [Parameter] public RenderFragment ChildContent { get; set; }
    }
    
    <!--组件使用-->
    <Component>
        <strong>Superior!</strong>
    </Component>
    

    《进击吧!Blazor!》第一章 5.组件开发

    Component具有一个类型为 RenderFragmentChildContent 属性,RenderFragment表示要呈现的 UI 段。 ChildContent 的值是从父组件接收的UI段。 在组件中需要呈现ChildContent内容的地方放置@ChildContent标记。 ChildContent属性命名为固定名字,下例是完整写法,上面是简略写法。

    <Component>
        <ChildContent>
            <strong>Superior!</strong>
        </ChildContent>
    </Component>
    

    模板

    可以通过指定一个或多个 RenderFragment 类型的组件参数来接收多个UI段。

    <!--组件代码-->
    <h1>@Title is @Quality</h1>
    
    @code{
        [Parameter] public RenderFragment Title { get; set; }
        [Parameter] public RenderFragment Quality { get; set; }
    }
    
    <!--组件使用-->
    <Component>
        <Title>
            <strong>Blazor</strong>
        </Title>
        <Quality>
            <strong>Superior!</strong>
        </Quality>
    </Component>
    

    模板参数

    可以定义 RenderFragment<TValue> 类型的组件参数来定义支持参数的模板。

    <!--组件代码-->
    @foreach (var item in Items)
    {
        <h4>@Title(item) is Superior!</h4>
    }
    @code{
        [Parameter] public RenderFragment<string> Title { get; set; }
        [Parameter] public IReadOnlyList<string> Items { get; set; }
    }
    
    <!--组件使用-->
    <Component Items="items">
        <Title Context="item">
            <strong>@item</strong>
        </Title>
    </Component>
    @code{
        List<string> items = new List<string> { ".Net", "C#", "Blazor" };
    }
    

    《进击吧!Blazor!》第一章 5.组件开发

    组件使用时通过IReadOnlyList<string> Items属性将内容传入组件,组件内部使用@foreach (var item in Items)将集合循环呈现,@Title(item)确定了插入位置,且给模板传入item的值,再外部通过Context="item"接收参数,最终实现模板的呈现。

    6.生命周期

    Blazor 框架包括同步和异步生命周期方法。一般情况下同步方法会先与异步方法执行。 我们可以重写生命周期方法的,以在组件初始化和呈现期间对组件执行其他操作。

    组件初始化

    《进击吧!Blazor!》第一章 5.组件开发

    组件状态改变

    《进击吧!Blazor!》第一章 5.组件开发

    组件销毁

    《进击吧!Blazor!》第一章 5.组件开发

    ToDo应用组件化改造

    任务信息

    重要任务不论是否是今天,我们都需要便捷的查看,所以我们需要做一个“重要任务”的页面。 这个页面显示内容和“我的一天”非常相似,所以我们可以抽象出一个TaskItem.razor组件,组件的Html以及样式基本是从ToDay.razor组件迁移过来。

    <Card Bordered="true" Size="small" Class="task-card">
        <div class="task-card-item">
            @{
                var finishClass = new ClassMapper().Add("finish").If("unfinish", () => Item.IsFinish == false);
            }
            <div class="@(finishClass.ToString())" @onclick="OnFinishClick">
                <Icon Type="check" Theme="outline" />
            </div>
            <div class="title" @onclick="OnCardClick">
    
                @if (TitleTemplate != null)
                {
                    @TitleTemplate
                }
                else
                {
                    <AntDesign.Text Strong> @Item.Title</AntDesign.Text>
                    <br />
                    <AntDesign.Text Type="@TextElementType.Secondary">
                        @Item.Description
                    </AntDesign.Text>
                }
            </div>
            <div class="del" @onclick="OnDelClick">
                <Icon Type="rest" Theme="outline" />
            </div>
            <div class="date">
                @Item.PlanTime.ToShortDateString()
                <br />
                @{
                    int? days = (int?)Item.Deadline?.Subtract(DateTime.Now.Date).TotalDays;
                }
                <span style="color:@(days switch { _ when days > 3 => "#ccc", _ when days > 0 => "#ffd800", _ => "#ff0000" })">
                    @Item.Deadline?.ToShortDateString()
                </span>
            </div>
            @if (ShowStar)
            {
                <div class="star" @onclick="OnStarClick">
                    <Icon Type="star" Theme="@(Item.IsImportant ? "fill" : "outline")" />
                </div>
            }
        </div>
    </Card>
    
    public partial class TaskItem
    {
        //任务内容
        [Parameter] public TaskDto Item { get; set; }
    
        //完成图标事件
        [Parameter] public EventCallback<TaskDto> OnFinish { get; set; }
        public async void OnFinishClick()
        {
            if (OnFinish.HasDelegate)
                await OnFinish.InvokeAsync(Item);
        }
    
        //条目点击事件
        [Parameter] public EventCallback<TaskDto> OnCard { get; set; }
        public async void OnCardClick()
        {
            if (OnCard.HasDelegate)
                await OnCard.InvokeAsync(Item);
        }
    
        //删除图标事件
        [Parameter] public EventCallback<TaskDto> OnDel { get; set; }
        public async void OnDelClick()
        {
            if (OnDel.HasDelegate)
                await OnDel.InvokeAsync(Item);
        }
    
        //重要图标事件
        [Parameter] public EventCallback<TaskDto> OnStar { get; set; }
        public async void OnStarClick()
        {
            if (OnStar.HasDelegate)
                await OnStar.InvokeAsync(Item);
        }
    
        //是否相似重要图标
        [Parameter] public bool ShowStar { get; set; } = true;
    
        //支持标题模板
        [Parameter] public RenderFragment TitleTemplate { get; set; }
    }
    

    @if (TitleTemplate != null) 如果外部传入了模板,那么就是显示模板,否则就使用默认格式显示。

    新建任务

    在“重要任务”和“我的一天”中均有添加任务的功能,我们也将他们抽象成NewTask.razor组件。

    <Divider Text="新任务"></Divider>
    @if (newTask != null)
    {
        <Spin Spinning="isNewLoading">
            <div class="task-input">
                <DatePicker Picker="@DatePickerType.Date" @bind-Value="@newTask.PlanTime" />
                <Input @bind-Value="@newTask.Title" OnkeyUp="OnInsertKey" />
                @if(ChildContent!=null )
                {
                    @ChildContent(newTask)
                }
            </div>
        </Spin>
    }
    
    public partial class NewTask
    {
        [Inject] public MessageService MsgSrv { get; set; }
        [Inject] public HttpClient Http { get; set; }
    
        [Parameter] public EventCallback<TaskDto> OnInserted { get; set; }
        [Parameter] public Func<TaskDto> NewTaskFunc { get; set; }
        [Parameter] public RenderFragment<TaskDto> ChildContent { get; set; }
    
        //新的任务
        TaskDto newTask { get; set; }
        private bool isNewLoading { get; set; }
    
        protected override void OnInitialized()
        {
            newTask = NewTaskFunc?.Invoke();
            base.OnInitialized();
        }
    
        async void OnInsertKey(KeyboardEventArgs e)
        {
            if (e.Code == "Enter")
            {
                if (string.IsNullOrWhiteSpace(newTask.Title))
                {
                    MsgSrv.Error($"标题必须填写");
                    return;
                }
                isNewLoading = true;
                var result = await Http.PostAsJsonAsync<TaskDto>($"api/Task/SaveTask", newTask);
                if (result.IsSuccessStatusCode)
                {
                    newTask.TaskId = await result.Content.ReadFromJsonAsync<Guid>();
                    await Task.Delay(1000);
                    if (OnInserted.HasDelegate) await OnInserted.InvokeAsync(newTask);
    
                    newTask = NewTaskFunc?.Invoke();
                }
                else
                {
                    MsgSrv.Error($"请求发生错误 {result.StatusCode}");
                }
                isNewLoading = false;
                StateHasChanged();
            }
        }
    }
    

    EventCallback<TaskDto> OnInserted 不同场景下插入后需要做的事情可能不同,所以通过这个事件由外部进行处理。 Func<TaskDto> NewTaskFunc 不同场景下对TaskDto初始化要求不同,所以用这个函数来调用初始化。 RenderFragment<TaskDto> ChildContent 使用模板实现额外的表单进行扩展输入内容。

    重要任务

    创建Star.razor文件作为重要任务的页面文件,代码如下

    @page "/star"
    
    <PageHeader Title="@("重要的任务")" Sub数量:{taskDtos?.Count}")"></PageHeader>
    
    <Spin Spinning="@isLoading">
        @foreach (var item in taskDtos)
        {
            <TaskItem  Item="item" OnFinish="OnFinish" OnCard="OnCardClick" OnDel="OnDel" OnStar="OnStar" ShowStar="false">
            </TaskItem>
        }
        <NewTask OnInserted="OnInsert" NewTaskFunc="() => new TaskDto() { PlanTime = DateTime.Now.Date, IsImportant = true  }"></NewTask>
    </Spin>
    
    public partial class Star
    {
        // 1、	列出当天的所有代办工作
        [Inject] public HttpClient Http { get; set; }
        
        bool isLoading = true;
        private List<TaskDto> taskDtos = new List<TaskDto>();
        protected async override Task OnInitializedAsync()
        {
            isLoading = true;
            taskDtos = await Http.GetFromJsonAsync<List<TaskDto>>("api/Task/GetStarTask");
            isLoading = false;
            await base.OnInitializedAsync();
        }
    
        //2、	添加代办
        public MessageService MsgSrv { get; set; }
        async void OnInsert(TaskDto item)
        {
            taskDtos.Add(item);
        }
    
        //3、	编辑抽屉
        [Inject] public TaskDetailServices TaskSrv { get; set; }
        async void OnCardClick(TaskDto task)
        {
            TaskSrv.EditTask(task, taskDtos);
            await InvokeAsync(StateHasChanged);
        }
    
        //4、	修改重要程度
        private async void OnStar(TaskDto task)
        {
            var req = new SetImportantReq()
            {
                TaskId = task.TaskId,
                IsImportant = !task.IsImportant,
            };
    
            var result = await Http.PostAsJsonAsync<SetImportantReq>("api/Task/SetImportant", req);
            if (result.IsSuccessStatusCode)
            {
                task.IsImportant = req.IsImportant;
                StateHasChanged();
            }
        }
    
        //5、	修改完成与否
        private async void OnFinish(TaskDto task)
        {
            var req = new SetFinishReq()
            {
                TaskId = task.TaskId,
                IsFinish = !task.IsFinish,
            };
    
            var result = await Http.PostAsJsonAsync<SetFinishReq>("api/Task/SetFinish", req);
            if (result.IsSuccessStatusCode)
            {
                task.IsFinish = req.IsFinish;
                StateHasChanged();
            }
        }
    
        //6、	删除代办
        [Inject] public ConfirmService ConfirmSrv { get; set; }
    
        public async Task OnDel(TaskDto task)
        {
            if (await ConfirmSrv.Show($"是否删除任务 {task.Title}", "删除", ConfirmButtons.YesNo, ConfirmIcon.Info) == ConfirmResult.Yes)
            {
                taskDtos.Remove(task);
            }
        }
    }
    

    《进击吧!Blazor!》第一章 5.组件开发 TaskItem OnFinish="OnFinish" OnCard="OnCardClick" OnDel="OnDel" OnStar="OnStar" 绑定不同的操作函数

    ShowStar="false" 不显示重要图标

    NewTask NewTaskFunc="() => new TaskDto() { PlanTime = DateTime.Now.Date, IsImportant = true }" 重要初始化时默认将IsImportant设置成true

    我的一天

    我们将“我的一天”也进行适当改造

    @page "/today"
    
    <PageHeader Title="@("我的一天")" Subyyyy年MM月dd日")"></PageHeader>
    
    <Spin Spinning="@isLoading">
        @foreach (var item in taskDtos)
        {
            <TaskItem @key="item.TaskId" Item="item" OnFinish="OnFinish" OnCard="OnCardClick" OnDel="OnDel" OnStar="OnStar">
                <TitleTemplate>
                    <AntDesign.Text Strong Style="@(item.IsFinish?"text-decoration: line-through;color:silver;":"")"> @item.Title</AntDesign.Text>
                    <br />
                    <AntDesign.Text Type="@TextElementType.Secondary">
                        @item.Description
                    </AntDesign.Text>
                </TitleTemplate>
            </TaskItem>
        }
    
        <NewTask OnInserted="OnInsert" NewTaskFunc="()=>  new TaskDto() {PlanTime=DateTime.Now.Date }">
            <ChildContent Context="newTask">
                <RadioGroup @bind-Value="newTask.IsImportant">
                    <Radio RadioButton Value="true">重要</Radio>
                    <Radio RadioButton Value="false">普通</Radio>
                </RadioGroup>
            </ChildContent>
        </NewTask>
    </Spin>
    

    《进击吧!Blazor!》第一章 5.组件开发 TaskItem TitleTemplate 通过模板重写了标题的显示方式,支持当完成后标题增加删除线

    NewTask ChildContent 重写了子内容,提供了重要度的选择。

    次回预告

    自己的待办当然只有自己能看了啦,所以登录,权限啥的都给安排上,请关注下一节——安全

    学习资料

    更多关于Blazor学习资料:https://aka.ms/LearnBlazor


    起源地下载网 » 《进击吧!Blazor!》第一章 5.组件开发

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元