如何从 UI 中分离代码 Blazor.Net
How to Separate Code From UI In Blazor.Net
参考这篇 VisualStudioMagazine 文章,我试图将代码放在单独的文件中而不是 razor 视图中。
我试过了:
@page "/Item"
@using WebApplication1.Shared
@using WebApplication1.Client.Services;
@inject HttpClient Http
@inherits ItemComponent
@if (ItemList != null)
{
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Category</th>
<th>Metal</th>
<th>Price</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
@foreach (var item in ItemList)
{
<tr>
<td>@item.ID</td>
<td>@item.Name</td>
<td>@item.Category</td>
<td>@item.Metal</td>
<td>@item.Price</td>
<td>@item.Quantity</td>
</tr>
}
</tbody>
</table>
}
@functions{
public ItemModel[] ItemList;
ItemComponent IC = new ItemComponent();
protected override async Task OnInitAsync()
{
ItemList = IC.GetItems().Result;
//ItemList = await Http.GetJsonAsync<ItemModel[]>("api/Item/GetItems");
StateHasChanged();
}
}
和项目组件:
using System.Threading.Tasks;
using WebApplication1.Shared;
using System.Net.Http;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Blazor;
namespace WebApplication1.Client.Services
{
public class ItemComponent
{
public async Task<ItemModel[]> GetItems()
{
ItemModel[] ItemList;
HttpClient Http = new HttpClient();
ItemList = await Http.GetJsonAsync<ItemModel[]>("api/Item/GetItems");
return ItemList;
}
}
}
但是不起作用,它表明:
Severity Code Description Project File Line Suppression State
Error CS0115 'Item.BuildRenderTree(RenderTreeBuilder)': no suitable method found to override WebApplication1.Client D:\Other\blazor\WebApplication1.Client\obj\Debug\netstandard2.0\RazorDeclaration\Pages\ItemModule\Item.razor.g.cs 30 Active
另外根据教程页面无法继承 BlazorComponent
到 ItemComponent
因为它没有引用。
有什么方法可以将大部分代码从 Blazor 视图分离到单独的代码文件中吗?
更新 1
按照 Chris 的回答进行更改后,显示异常
System.Net.Http.HttpRequestException: No connection could be made because the target machine actively refused it. --->
System.Net.Sockets.SocketException: No connection could be made
because the target machine actively refused it. at
System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port,
CancellationToken cancellationToken) --- End of inner exception
stack trace --- at
System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port,
CancellationToken cancellationToken) at
System.Threading.Tasks.ValueTask1.get_Result() at
System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage
request, Boolean allowHttp2, CancellationToken cancellationToken)<br>
at System.Threading.Tasks.ValueTask
1.get_Result() at
System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage
request, CancellationToken cancellationToken) at
System.Threading.Tasks.ValueTask1.get_Result() at
System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage
request, CancellationToken cancellationToken) at
System.Threading.Tasks.ValueTask
1.get_Result() at
System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage
request, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage
request, CancellationToken cancellationToken) at
System.Net.Http.HttpClient.FinishSendAsyncUnbuffered(Task1 sendTask,
HttpRequestMessage request, CancellationTokenSource cts, Boolean
disposeCts) at System.Net.Http.HttpClient.GetStringAsyncCore(Task
1
getTask) at
Microsoft.AspNetCore.Builder.BlazorMonoDebugProxyAppBuilderExtensions.GetOpenedBrowserTabs(String
debuggerHost) at
Microsoft.AspNetCore.Builder.BlazorMonoDebugProxyAppBuilderExtensions.DebugHome(HttpContext
context)
您只需要像这样在 ItemComponent
class 中继承 ComponentBase
。
public class ItemComponent : ComponentBase
{
public async Task<ItemModel[]> GetItems()
{
ItemModel[] ItemList;
HttpClient Http = new HttpClient();
ItemList = await Http.GetJsonAsync<ItemModel[]>("api/Item/GetItems");
return ItemList;
}
}
这篇文章有点过时了,因为 BlazorComponent
刚刚重命名。
只需确保将视图的 functions
块中的所有代码移到基 class 中,因为混合使用这两种方法可能会产生奇怪的副作用。
你有两个选择。第一个已经被Chris Sainty提到了。创建一个继承自 ComponentBase 的 class 并在你的 Razor 视图中继承它。
您的 class 将定义为:
public class MyBaseClass : ComponentBase
并且在你的 Razor 视图中你使用:
@inherits MyBaseClass
这使得 MyBaseClass 成为 Razor 视图的代码隐藏页面,并且它能够覆盖视图的所有生命周期事件。
第二个选项是创建一个 ViewModel。您创建一个标准 C# class 并使用 属性 注入将其注入到您的 Razor 视图中。
您通常定义 class:
public class MyViewModel
并将其注入您的 Razor 视图:
@inject MyViewModel
此 ViewModel class 不了解页面生命周期事件,并且不依赖于任何与 Blazor 相关的内容。如果你只是想将你的 Razor 视图绑定到一个对象并且需要一些可以重复使用的东西(或者想把它放在一个共享项目中)这可能是一个不错的选择。
如果您需要或希望将页面生命周期代码与数据绑定分开,您可以在同一个 Razor 视图上使用继承的代码和注入的 ViewModel。
这里还有一个类似于的解决方案:
You can use an inherited code behind and an injected ViewModel on the
same Razor View if you have a need to or if you want to keep page
lifecycle code separate from your data bindings.
考虑 'state' 作为 view-models
的替代方案
近年来,关于使用 'State' 概念管理应用程序当前状态的讨论很多。自从 Flux 模式(尤其是 Redux 实现)兴起以来,这在 React(以及现在的其他 JS 框架)世界中特别流行。
state 和 a view-model 有什么区别?
A view-model 通常表示特定页面的状态,并且通常包含与该页面呈现方式相关的属性(例如 select 列表的数据,额外的 属性 来说明页面的某个部分是否应该可见等),还有一个 属性 来保存包含要绑定到该页面的数据的对象(例如 SalesOrder
class说)。
基于状态的方法做的事情大致相同,但不是按适用于的页面对状态进行分组(如 view-model 那样),基于状态的方法通常按行为对代码进行分组(例如,所有状态与订购比萨饼有关,所以当前比萨饼包含什么以及如果订单正在处理中应该显示哪些 UI 元素)并认识到状态可能由多个组件显示 - 所以 State 对象不会'不一定像 ViewModel 通常那样直接映射到单个 razor 文件。
为什么要采用状态方法?
基于状态的方法有两个主要好处:
- 因为状态 class 不依赖于 UI class 或框架(所以没有引用 Blazor、Razor 等)它可以像任何其他 C# class。这意味着你可以,例如通过仅测试
MyState.SaveButtonEnabled' property is
true`,检查当数据 class 上的 属性 设置为特定值时按钮是否会被禁用。这比尝试通过 UI 自动化或类似方法测试行为要简单得多。
- 基于状态的方法考虑了这样一个事实,即应用程序中某个功能区域的状态通常会跨越多个组件或页面。对于较小的单页应用程序 (SPA),通常用一个状态对象来表示整个应用程序就足够了。显然,这种方法只适用于整个应用程序在用户会话期间存在的 SPA。
.NET 团队提供的优秀示例和教程
举个例子就容易多了,谢天谢地,Microsoft Blazor 团队的 Blazing Pizza's blazor-workshop 提供了一个极好的例子。
作为该教程中的一个简单示例 - 这是 OrderState
class 保存与 in-progress 订单相关的当前状态:
public class OrderState
{
public event EventHandler StateChanged;
public bool ShowingConfigureDialog { get; private set; }
public Pizza ConfiguringPizza { get; private set; }
public Order Order { get; private set; } = new Order();
public void ShowConfigurePizzaDialog(PizzaSpecial special)
{
ConfiguringPizza = new Pizza()
{
Special = special,
SpecialId = special.Id,
Size = Pizza.DefaultSize,
Toppings = new List<PizzaTopping>(),
};
ShowingConfigureDialog = true;
}
public void CancelConfigurePizzaDialog()
{
ConfiguringPizza = null;
ShowingConfigureDialog = false;
StateHasChanged();
}
public void ConfirmConfigurePizzaDialog()
{
Order.Pizzas.Add(ConfiguringPizza);
ConfiguringPizza = null;
ShowingConfigureDialog = false;
StateHasChanged();
}
public void RemoveConfiguredPizza(Pizza pizza)
{
Order.Pizzas.Remove(pizza);
StateHasChanged();
}
public void ResetOrder()
{
Order = new Order();
}
private void StateHasChanged()
{
StateChanged?.Invoke(this, EventArgs.Empty);
}
} ```
请注意,此状态 class 没有绑定到它的 UI 的概念,但它确实具有控制 UI 行为的属性。
剃刀 classes 在该示例中也仍然具有 @functions 块,但是通过在 State class 中引入在控制 UI 行为(例如 ShowingConfigureDialog
)。例如,从 index.razor:
<ul class="pizza-cards">
@if (specials != null)
{
@foreach (var special in specials)
{
<li onclick="@(() => OrderState.ShowConfigurePizzaDialog(special))"
style="background-image: url('@special.ImageUrl')">
<div class="pizza-info">
<span class="title">@special.Name</span>
@special.Description
<span class="price">@special.GetFormattedBasePrice()</span>
</div>
</li>
}
}
</ul> </div> ```
整个教程都很棒,我强烈建议您通读一遍。
但我不想在我的 razor 文件中使用 C# 代码...
您仍然可以将 @code
块中的代码放入 base-class 的文件中,也可以使用状态方法。
人们倾向于不这样做的原因是,如果您的状态文件正在驱动 UI 行为,那么 @code
接线代码通常只会以几行结束,因此通常不会似乎值得放在一个单独的文件中。
我已经阅读了有关父 class 方法的文章,该方法通过创建一个 class 继承自 ComponentBase 并简单地从组件中的基础 class 继承。我不是粉丝,因为它迫使我将应该维护 internally/privately 的 class 结构暴露给 class,并跟踪受保护的继承,我想这是正确的答案。
但是,我可能在这里遗漏了一些东西所以请不要因为我推荐这个而屠杀我,但是为什么你不能只使用部分指令,创建一个'sidecar'(我的术语)文件 ComponentName.razor.cs 并简单地将 class 声明为部分 class。我试过了,效果很好……
使用当前的写作模板项目,在 Counter 组件中,我简单地剥离了所有代码,结果如下:
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
然后我开始创建 sidecar 文件 Counter.razor.cs 并填充了:
using Microsoft.AspNetCore.Components;
namespace FirstBlazorWasm.Pages //my test namespace
{
public partial class Counter //<--- note the partial class definition
{
private int currentCount;
private void IncrementCount()
{
currentCount++;
}
}
}
叫我 2003 年先生,但它有效。 :)
参考这篇 VisualStudioMagazine 文章,我试图将代码放在单独的文件中而不是 razor 视图中。
我试过了:
@page "/Item"
@using WebApplication1.Shared
@using WebApplication1.Client.Services;
@inject HttpClient Http
@inherits ItemComponent
@if (ItemList != null)
{
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Category</th>
<th>Metal</th>
<th>Price</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
@foreach (var item in ItemList)
{
<tr>
<td>@item.ID</td>
<td>@item.Name</td>
<td>@item.Category</td>
<td>@item.Metal</td>
<td>@item.Price</td>
<td>@item.Quantity</td>
</tr>
}
</tbody>
</table>
}
@functions{
public ItemModel[] ItemList;
ItemComponent IC = new ItemComponent();
protected override async Task OnInitAsync()
{
ItemList = IC.GetItems().Result;
//ItemList = await Http.GetJsonAsync<ItemModel[]>("api/Item/GetItems");
StateHasChanged();
}
}
和项目组件:
using System.Threading.Tasks;
using WebApplication1.Shared;
using System.Net.Http;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Blazor;
namespace WebApplication1.Client.Services
{
public class ItemComponent
{
public async Task<ItemModel[]> GetItems()
{
ItemModel[] ItemList;
HttpClient Http = new HttpClient();
ItemList = await Http.GetJsonAsync<ItemModel[]>("api/Item/GetItems");
return ItemList;
}
}
}
但是不起作用,它表明:
Severity Code Description Project File Line Suppression State Error CS0115 'Item.BuildRenderTree(RenderTreeBuilder)': no suitable method found to override WebApplication1.Client D:\Other\blazor\WebApplication1.Client\obj\Debug\netstandard2.0\RazorDeclaration\Pages\ItemModule\Item.razor.g.cs 30 Active
另外根据教程页面无法继承 BlazorComponent
到 ItemComponent
因为它没有引用。
有什么方法可以将大部分代码从 Blazor 视图分离到单独的代码文件中吗?
更新 1
按照 Chris 的回答进行更改后,显示异常
System.Net.Http.HttpRequestException: No connection could be made because the target machine actively refused it. ---> System.Net.Sockets.SocketException: No connection could be made because the target machine actively refused it. at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken) --- End of inner exception stack trace --- at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken) at System.Threading.Tasks.ValueTask
1.get_Result() at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean allowHttp2, CancellationToken cancellationToken)<br> at System.Threading.Tasks.ValueTask
1.get_Result() at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) at System.Threading.Tasks.ValueTask1.get_Result() at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) at System.Threading.Tasks.ValueTask
1.get_Result() at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) at System.Net.Http.HttpClient.FinishSendAsyncUnbuffered(Task1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts) at System.Net.Http.HttpClient.GetStringAsyncCore(Task
1 getTask) at Microsoft.AspNetCore.Builder.BlazorMonoDebugProxyAppBuilderExtensions.GetOpenedBrowserTabs(String debuggerHost) at Microsoft.AspNetCore.Builder.BlazorMonoDebugProxyAppBuilderExtensions.DebugHome(HttpContext context)
您只需要像这样在 ItemComponent
class 中继承 ComponentBase
。
public class ItemComponent : ComponentBase
{
public async Task<ItemModel[]> GetItems()
{
ItemModel[] ItemList;
HttpClient Http = new HttpClient();
ItemList = await Http.GetJsonAsync<ItemModel[]>("api/Item/GetItems");
return ItemList;
}
}
这篇文章有点过时了,因为 BlazorComponent
刚刚重命名。
只需确保将视图的 functions
块中的所有代码移到基 class 中,因为混合使用这两种方法可能会产生奇怪的副作用。
你有两个选择。第一个已经被Chris Sainty提到了。创建一个继承自 ComponentBase 的 class 并在你的 Razor 视图中继承它。
您的 class 将定义为:
public class MyBaseClass : ComponentBase
并且在你的 Razor 视图中你使用:
@inherits MyBaseClass
这使得 MyBaseClass 成为 Razor 视图的代码隐藏页面,并且它能够覆盖视图的所有生命周期事件。
第二个选项是创建一个 ViewModel。您创建一个标准 C# class 并使用 属性 注入将其注入到您的 Razor 视图中。
您通常定义 class:
public class MyViewModel
并将其注入您的 Razor 视图:
@inject MyViewModel
此 ViewModel class 不了解页面生命周期事件,并且不依赖于任何与 Blazor 相关的内容。如果你只是想将你的 Razor 视图绑定到一个对象并且需要一些可以重复使用的东西(或者想把它放在一个共享项目中)这可能是一个不错的选择。
如果您需要或希望将页面生命周期代码与数据绑定分开,您可以在同一个 Razor 视图上使用继承的代码和注入的 ViewModel。
这里还有一个类似于
You can use an inherited code behind and an injected ViewModel on the same Razor View if you have a need to or if you want to keep page lifecycle code separate from your data bindings.
考虑 'state' 作为 view-models
的替代方案近年来,关于使用 'State' 概念管理应用程序当前状态的讨论很多。自从 Flux 模式(尤其是 Redux 实现)兴起以来,这在 React(以及现在的其他 JS 框架)世界中特别流行。
state 和 a view-model 有什么区别?
A view-model 通常表示特定页面的状态,并且通常包含与该页面呈现方式相关的属性(例如 select 列表的数据,额外的 属性 来说明页面的某个部分是否应该可见等),还有一个 属性 来保存包含要绑定到该页面的数据的对象(例如 SalesOrder
class说)。
基于状态的方法做的事情大致相同,但不是按适用于的页面对状态进行分组(如 view-model 那样),基于状态的方法通常按行为对代码进行分组(例如,所有状态与订购比萨饼有关,所以当前比萨饼包含什么以及如果订单正在处理中应该显示哪些 UI 元素)并认识到状态可能由多个组件显示 - 所以 State 对象不会'不一定像 ViewModel 通常那样直接映射到单个 razor 文件。
为什么要采用状态方法?
基于状态的方法有两个主要好处:
- 因为状态 class 不依赖于 UI class 或框架(所以没有引用 Blazor、Razor 等)它可以像任何其他 C# class。这意味着你可以,例如通过仅测试
MyState.SaveButtonEnabled' property is
true`,检查当数据 class 上的 属性 设置为特定值时按钮是否会被禁用。这比尝试通过 UI 自动化或类似方法测试行为要简单得多。 - 基于状态的方法考虑了这样一个事实,即应用程序中某个功能区域的状态通常会跨越多个组件或页面。对于较小的单页应用程序 (SPA),通常用一个状态对象来表示整个应用程序就足够了。显然,这种方法只适用于整个应用程序在用户会话期间存在的 SPA。
.NET 团队提供的优秀示例和教程
举个例子就容易多了,谢天谢地,Microsoft Blazor 团队的 Blazing Pizza's blazor-workshop 提供了一个极好的例子。
作为该教程中的一个简单示例 - 这是 OrderState
class 保存与 in-progress 订单相关的当前状态:
public class OrderState { public event EventHandler StateChanged; public bool ShowingConfigureDialog { get; private set; } public Pizza ConfiguringPizza { get; private set; } public Order Order { get; private set; } = new Order(); public void ShowConfigurePizzaDialog(PizzaSpecial special) { ConfiguringPizza = new Pizza() { Special = special, SpecialId = special.Id, Size = Pizza.DefaultSize, Toppings = new List<PizzaTopping>(), }; ShowingConfigureDialog = true; } public void CancelConfigurePizzaDialog() { ConfiguringPizza = null; ShowingConfigureDialog = false; StateHasChanged(); } public void ConfirmConfigurePizzaDialog() { Order.Pizzas.Add(ConfiguringPizza); ConfiguringPizza = null; ShowingConfigureDialog = false; StateHasChanged(); } public void RemoveConfiguredPizza(Pizza pizza) { Order.Pizzas.Remove(pizza); StateHasChanged(); } public void ResetOrder() { Order = new Order(); } private void StateHasChanged() { StateChanged?.Invoke(this, EventArgs.Empty); } } ```
请注意,此状态 class 没有绑定到它的 UI 的概念,但它确实具有控制 UI 行为的属性。
剃刀 classes 在该示例中也仍然具有 @functions 块,但是通过在 State class 中引入在控制 UI 行为(例如 ShowingConfigureDialog
)。例如,从 index.razor:
<ul class="pizza-cards"> @if (specials != null) { @foreach (var special in specials) { <li onclick="@(() => OrderState.ShowConfigurePizzaDialog(special))" style="background-image: url('@special.ImageUrl')"> <div class="pizza-info"> <span class="title">@special.Name</span> @special.Description <span class="price">@special.GetFormattedBasePrice()</span> </div> </li> } } </ul> </div> ```
整个教程都很棒,我强烈建议您通读一遍。
但我不想在我的 razor 文件中使用 C# 代码...
您仍然可以将 @code
块中的代码放入 base-class 的文件中,也可以使用状态方法。
人们倾向于不这样做的原因是,如果您的状态文件正在驱动 UI 行为,那么 @code
接线代码通常只会以几行结束,因此通常不会似乎值得放在一个单独的文件中。
我已经阅读了有关父 class 方法的文章,该方法通过创建一个 class 继承自 ComponentBase 并简单地从组件中的基础 class 继承。我不是粉丝,因为它迫使我将应该维护 internally/privately 的 class 结构暴露给 class,并跟踪受保护的继承,我想这是正确的答案。
但是,我可能在这里遗漏了一些东西所以请不要因为我推荐这个而屠杀我,但是为什么你不能只使用部分指令,创建一个'sidecar'(我的术语)文件 ComponentName.razor.cs 并简单地将 class 声明为部分 class。我试过了,效果很好……
使用当前的写作模板项目,在 Counter 组件中,我简单地剥离了所有代码,结果如下:
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
然后我开始创建 sidecar 文件 Counter.razor.cs 并填充了:
using Microsoft.AspNetCore.Components;
namespace FirstBlazorWasm.Pages //my test namespace
{
public partial class Counter //<--- note the partial class definition
{
private int currentCount;
private void IncrementCount()
{
currentCount++;
}
}
}
叫我 2003 年先生,但它有效。 :)