简体   繁体   中英

Blazor pages and components code inside razor page ? (Blazor patterns)

Is there some specific pattern to write code when dealing with Blazor in razor pages? All samples about Blazor I see tosses HTML/Razor/CSS and C# code in the same file, like:

<div class="text">@sometext</div>

<style> .text{ color:black; } </style>

@code{
   sometext = "text.."
}

Is there some pattern to separate C# and HTML maybe like in MVC? I moved most logic from @code{} to separate Service class for pages.

I'm interested to hear different approaches to this, what's the best practice?

The actual question asks two different things and requires a very long answer.

There are several patterns that can be used, eg MVVM, MVU. Blazor WebAssembly is roughly React# so using the same component logic used in React helps a lot .

Both frameworks build SPAs, so the concerns are the same, so the solutions are similar. And just like all SPA frameworks, MVC was replace by MVVM almost a decade ago with eg Backbone. State management between multiple components emerged as a separate concern so SPAs have different patterns and mechanisms to manage state it (think React's Redux and alternatives).

And even storage is non-trivial. ASP.NET Core Blazor state management describes storage in multiple places:

  • A server database
  • Query parameters
  • Browser local storage
  • In-memory caches

Multiple patterns can be used - React-like components with the master state held in a ViewModel at the "parent" level, wherever that may be.

In Blazor's data binding, like React, data and state flow downwardsfrom the parent through component Parameter properties. Changes to the data flow upwards through EventCallback s. That makes implementing patterns like MVVM and MVU easy.

Component-level design

In React (and Blazor), data and state flow downwards from the parent. Changes to the data flow upwards. Blazor's data binding is built to facilitate exactly this, although it doesn't restrict yo:

  • The actual state is held by the parent, in whatever form the parent wants. It may be multiple properties, a model/DTO class or a ViewModel that follow. The data is passed to nested components through component Parameter properties .
  • Changes, eg in response to events are signaled through EventCallback s. The nested properties don't store the changed data themselves though.

In this example copied from the docs, the ChildBind component never changes the Year property itself. It tells its parent the year changed, and leaves it to the parent (and data binding) to tell it what to redraw :

Shared/ChildBind.razor

<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    private Random r = new();

    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild()
    {
        await YearChanged.InvokeAsync(r.Next(1950, 2021));
    }
}

It's possible to bind across more than 2 components with a bit of work or by using Cascading values and parameters .

Application architecture - Overview

The obvious way to design a complex SPA both in React and Blazor is to compose components. Both frameworks are built to work that way. What's not so obvious is how to move data/state and events among those components, who's going to store the data, who's going to change it, how changes end up modifying the UI etc.

And since we talk about SPAs, even the "where" has no clear answer, with state kept in query parameters, browser storage or remote services.

Picking an architecture depends on the problem that needs solving. There's no best practice . The definition of a pattern is

A solution to a problem in a context

This means that what is a good solution in one case won't be a good solution in another.

Application architecture 0 - Component composition

That's almost the "default" way of working with Blazor. Use multiple pages which contain components. Data goes down, changes go up, trying to go sideways is essentially impossible so developers have to follow the flow.

If a page or component gets too big, it's easy to extract it into a separate component with its own Parameter s.

In some cases, component composition will be enough, especially when there's not a lot of complex state to manage or there aren't that many dependencies between components. Those components could call their own server-side APIs without increasing complexity. Different people could easily work on their own components independently up to a point.

Many quick projects start this way before becoming large enough to require a more complex architecture. When that happens, using components makes it far easier to adopt another architecture.

In eg an ERP or CRM application though, there's a lot of complex data to manage for even a single Customer. The same page may display multiple tabs coming from multiple systems, in summary or detail views. A single change may affect the display of multiple components on the same page.

Never mind that such applications often need to combine UI elements from different applications, and still behave as a single application. 15 years ago these were called Composite Applications or Mashups . Nowadays people talk about Micro-Frontends

Application architecture 1- MVVM

This pattern is used a lot in SPAs and WPF. In this design a separate ViewModel is used for every View with the actual state stored in a Model . The Model could be hiding a database or service calls. Multiple components that implement their own View/ViewModel s can work with the same Model , eg displaying current user data in different ways on every page.

There are a lot of articles on MVVM. I copied the image from MVVM Pattern in Blazor For State Management – A Complete Guide .

在此处输入图片说明

Communication between View and ViewModel uses Blazor data-binding. The ViewModel class itself needs to implement INotifyPropertyChanged , the same way it does in WPF, so changes to its properties can trigger View updates.

Application Architecture - MVU

This pattern originated in the Elm language, so it's also called the Elm Architecture . It's rising in popularity and .NET MAUI works on a variant of MVU. This article describes how it works in F#.

The high-level image shows its major feature - flow is one-way only. The Update part generates changes that are used to update the Model . Sounds a lot like Redux , doesn't it?

在此处输入图片说明

The following image comes from Comet , an MVU pattern for .NET MAUI and makes it clearer how the concepts map to Blazor concepts like data binding:

在此处输入图片说明

In this case that component Parameter s offer one-way binding but EventCallback s need to generate updates for the Model instead of notifying the parent component.

In Redux , both Command s and Update s are top-level concepts, defined in advance, similar to messages, so the application state and components can be changed independent of each other.

Separating Code from Markup

The Partial class support section in the Blazor Component Overview page shows you can put the C# code in a code-behind file containing a partial class whose name is the same as your component. During compilation the code from all partial classes is merged into a single one and compiled as a single class.

Borrowing from the docs, if you have a component named Pages/Counter.razor :

page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

You can split it into a .razor and a .cs file:

CounterPartialClass.razor

@page "/counter-partial-class"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

CounterPartialClass.razor.cs

namespace BlazorSample.Pages
{
    public partial class CounterPartialClass
    {
        private int currentCount = 0;

        void IncrementCount()
        {
            currentCount++;
        }
    }
}

As an add on to the other answers given:

  1. Keep the Razor CLEAN - see below.
@if (records != null && record.Count > 0 )
{
  ....
}

becomes:

@if (hasRecords)
{
  ....
}

@code {
....

bool hasRecords => records != null && record.Count > 0;
}
  1. Use components - don't keep repeating the same blocks of markup. You would never do that in C# code!
<tr>
  <td class="table-row p-2 ...">
   @Value
  </td>
   .....
</tr>

becomes:

MyCellComponent

  <td class="table-row p-2 ...">
   @ChildContent
  </td>

@code {

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

and the table

<tr>
  <MyCellComponent>
     @Value
  </MyCellComponent>
  ....
</tr>

Makes changing the padding across a load a tables easy!

As for application framework and coding patterns, I've tried to summarise mine here demonstrating one way to build a database driven Blazor application. Building-a-Database-Application-in-Blazor .

One approach is to seperate this content into 3 pieces. All in same directory.

Counter.razor 
Counter.razor.cs
Counter.razor.css

In that way you can separate the workflow into the logical pieces(htlm/razor, cs, css) and code is more clear.

In terms of patterns: Our team generally considers the razor file to be a single concern (in 'separation of concerns' terms) where that concern is rendering the data in some model to the page. On that basis we don't separate the styling, HTML and code out into separate files.

That approach works for us because we are generally binding the elements of the razor page to a separate state object of some sort; either a view-model or a central state store (such as Redux or the like), so the code section stays fairly small.

I've discussed this a bit more in my answer about the use of state vs view-models in Blazor .

Microsoft have some good examples in their dotnet-architecture/eShopOnWeb tutorial project. They also keep everything in the same file rather than splitting the razor page's contents out into separate files. The Catalog Item Edit Page is an example of that.

As a general principle, Blazor is not opinionated on which pattern to use or how to separate things out. That's by design (I will try and dig out a citation for that), it's a deliberate decision by the Blazor team to enable a number of ways to do this. So, the decision as to whether to separate parts of the content to different files is not Blazor specific, but should be driven by the same coding principles you would use anywhere, eg SOLID principles and the standard conventions of the UX design pattern your using for your Blazor pages (eg MVVM).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM