我将作为将服务隔离到基类的大力支持者加入进来。在得出这个结论之前,我一直遇到的问题是,随着应用程序大小和复杂性的增加,将服务调用分散到各处会变得令人困惑。将每个组件构建为一个原子的东西来处理它自己的所有事情并注入它的服务是非常诱人的,但是一旦所有这些组件开始组合在一起并且需要开始相互通信,它就会变得非常令人头疼。当你有类似单例的东西时,这会复合,其中任何状态都可能涉及,因为一个组件的底层状态可以很容易地被另一个组件更改。(有时不是故意的 - 请参阅 EF Core 和数据跟踪以及当被跟踪的数据从 2 个组件中引用时您可以获得的乐趣 - 或者更糟,
实现组件自治的第二条途径是使用级联参数,但无论何时,您都是将组件耦合到 DOM 树上某处的具体组件,避免耦合是 SOLID 的重点。通常最好让每个组件都代表非常简单的功能,这些功能可以组合起来为用户创造更丰富的体验。
因此,我发现成功的地方是隔离您在基类中提到的服务,然后将 DOM 树中的每个组件尽可能地保持不变,这对我的输出以及查找和修复错误的能力产生了巨大影响. 事实上,在我开始使用这种方法之前,我有一个项目不得不放弃两次,现在我在一个功能性应用程序中,并以一个很好的剪辑构建功能。(感谢上帝,这是一个爱好项目!)
这个方法一点也不复杂。在基类中,我将在需要时将方法调用和属性公开为受保护的,并尽可能保持其他所有内容为私有,因此外部可见性处于绝对最低限度。所有服务调用也发生在基类中,并封装在私有方法中,这会破坏服务和 UI 之间的连接。然后我会将数据作为组件参数向下传递到 DOM 树,并将功能作为 type 的参数向下传递EventCallback<T>
。
以经典的订单列表为例。我可以通过客户 ID 加载订单列表,然后通过使用表达式主体成员过滤主列表来公开打开的订单和关闭的订单的列表。所有这些都发生在基类中,但我对其进行了设置,因此 UI 可以访问的唯一内容是子列表和方法。在下面的示例中,我通过控制台日志表示服务调用,但您会明白这一点,您在问题中提到构建事物的方式本质上是这样的:
OrdersBase.cs
public class OrdersBase : ComponentBase
{
private List<Order> _orders = new List<Order>();
protected List<Order> OpenOrders => _orders.Where(o => o.IsClosed == false).ToList();
protected List<Order> ClosedOrders => _orders.Where(o => o.IsClosed == true).ToList();
protected void CloseOrder(Order order)
{
_orders.Find(o => o.Id == order.Id).IsClosed = true;
Console.WriteLine($"Service was called to close order #{order.Id}");
}
protected void OpenOrder(Order order)
{
_orders.Find(o => o.Id == order.Id).IsClosed = false;
Console.WriteLine($"Service was called to open order #{order.Id}");
}
protected override async Task OnInitializedAsync()
{
Console.WriteLine("Calling service to fill the orders list for customer #1...");
// quick mock up for a few orders
_orders = new List<Order>()
{
new Order() { Id = 1, OrderName = "Order Number 1", CustomerId = 1 },
new Order() { Id = 2, OrderName = "Order Number 2", CustomerId = 1 },
new Order() { Id = 3, OrderName = "Order Number 3", CustomerId = 1 },
new Order() { Id = 4, OrderName = "Order Number 4", CustomerId = 1 },
new Order() { Id = 5, OrderName = "Order Number 5", CustomerId = 1 },
};
Console.WriteLine("Order list filled");
}
}
现在我可以使用顶级组件中的基类,并且只能访问受保护的和公共的成员。我可以使用这个高级组件来编排 UI 的排列方式并为委托传递方法,这就是它所要做的一切。结果这很轻。
订单.razor
@page "/orders"
@inherits OrdersBase
<div>
<h3>Open Orders:</h3>
<OrdersList Orders="OpenOrders" OnOrderClicked="CloseOrder" />
</div>
<div>
<h3>Closed Orders:</h3>
<OrdersList Orders="ClosedOrders" OnOrderClicked="OpenOrder" />
</div>
然后,OrderList 组件负责呈现 OrderItem 列表并传递委托操作。同样,只是一个简单的、愚蠢的组件。
OrderList.razor
<div>
@foreach (var order in Orders)
{
<OrderItem Order="order" OnOrderClicked="OnOrderClicked.InvokeAsync" />
}
</div>
@code {
[Parameter]
public List<Order> Orders { get; set; }
[Parameter]
public EventCallback<Order> OnOrderClicked { get; set; }
}
现在 OrderItem 列表可以呈现有关订单的内容并充当单击目标,当单击订单时,它会一直调用委托返回到基类,这就是方法运行的地方。OrderClicked 方法还检查 EventCallback,因此如果没有分配委托,则单击不会执行任何操作。
OrderItem.razor
<div @onclick="OrderClicked">
<p>Order Name: @Order.OrderName</p>
</div>
@code {
[Parameter]
public Order Order { get; set; }
[Parameter]
public EventCallback<Order> OnOrderClicked { get; set; }
private void OrderClicked()
{
if(OnOrderClicked.HasDelegate)
{
OnOrderClicked.InvokeAsync(Order);
}
}
}
所有这些共同构成显示订单的组件,如果您单击未结订单,它会移动已关闭的订单列表,反之亦然。所有逻辑都在基类中,每个组件都有一个简单的工作要做,这使得推理变得更加容易。
这也可以让我知道何时需要将组件分解为更小的组件。我的理念是一次不应该向用户展示太多,所以每个页面都应该简洁、简单,并且不要期望做太多事情。为此,当我构建这样的东西时,我可以告诉我,当我的基类或父 UI razor 文件开始膨胀时,我会走得很远,这会促使将部分功能重构到另一个专用页面。它可以生成更多文件,但也使构建和维护变得更加容易。
事实证明,这是对一个简短问题的长答案。您可能同意我的观点,也可能不同意,但希望它可以帮助您决定如何进行。