Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Separate Code From UI In Blazor.Net

Reference to this VisualStudioMagazine article, I am trying to have code in a separate file instead of razor view.

I tried:

@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();
    }
}

And ItemComponent:

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;
        }

    }
}

But it does not work , it shows that:

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

Also as per tutorial page can not inherit BlazorComponent to ItemComponent because it failed to have reference.

Is there any way to separate most of the code from Blazor view to a separate code file?

Update 1

After Making changes as per Chris Answer, it show exception

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)
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.ValueTask1.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(Task1 getTask) at Microsoft.AspNetCore.Builder.BlazorMonoDebugProxyAppBuilderExtensions.GetOpenedBrowserTabs(String debuggerHost) at Microsoft.AspNetCore.Builder.BlazorMonoDebugProxyAppBuilderExtensions.DebugHome(HttpContext context)

like image 261
Saurabh Avatar asked May 20 '19 08:05

Saurabh


4 Answers

You just need to inherit from ComponentBase in your ItemComponent class like this.

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;
    }
}

The article is a little out of date as BlazorComponent was renamed a while ago.

Just make sure to move all of the code you have in the functions block of your view into the base class as mixing the two approaches can have odd side effects.

like image 169
Chris Sainty Avatar answered Nov 06 '22 18:11

Chris Sainty


You have two options. The first was already mentioned by Chris Sainty. Create a class that inherits from ComponentBase and inherit it in your Razor view.

Your class would be defined as: public class MyBaseClass : ComponentBase

And in your Razor view you use: @inherits MyBaseClass

This makes MyBaseClass become a code behind page for your Razor view and it is able to override all of the lifecycle events for the view.

The second option is to create a ViewModel. You create a standard C# class and inject it into your Razor view using property injection.

You define your class normally: public class MyViewModel

And inject it into your Razor view: @inject MyViewModel

This ViewModel class is not aware of the page lifecycle events and has no dependencies on anything Blazor related. If you just want to bind your Razor view to an object and need something that can be reused (or want to put it in a shared project) this can be a good choice.

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.

like image 42
Louis Hendricks Avatar answered Nov 06 '22 18:11

Louis Hendricks


There's also another solution here which is similar to Louis Hendrick's point that:

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.

Consider 'state' as an alternative to view-models

In recent years there has been much talk of managing the current status of an application using the concept of 'State'. This is something that's been particularly popular in the React (and now other JS frameworks) world since the rise of the Flux pattern (and in particular the Redux implementation).

What's the difference between state and a view-model?

A view-model typically represents the state of a particular page and will often comprise of properties related to how that page is rendered (e.g. the data for a select list, an extra property to say if a section of the page should be visible etc.) and also a property that holds the object with the data to be bound on that page (e.g. a SalesOrder class say).

The State based approach does much the same thing, but instead of grouping state by the page that applies to (as a view-model does) the state based approach often groups code by behaviour (e.g. all the state to do with ordering a Pizza, so what the current Pizza comprises of and also what UI elements should be shown if an order is in process) and recognises that state may be displayed by multiple components - so the State objects won't necessarily map directly to a single razor file in the way that a ViewModel typically would.

Why take the state approach?

The state based approach has two main benefits:

  1. Because the state class has no dependency on the UI class or framework (so no reference to Blazor, Razor etc.) it can be tested like any other C# class. That means you can e.g. check if a button will be disabled when a property on the data class is set to a certain value by just testing that MyState.SaveButtonEnabled' property is true`. That's much simpler than trying to test behaviour via UI automation or the like.
  2. The state based approach takes account of the fact that the state of an area of functionality in an app often crosses several components or pages. For smaller Single Page Apps (SPAs) it's often sufficient to have a single state object to represent the whole app. Obviously that approach only really works for a SPA where the whole app lives for the duration of the user's session.

An excellent example and tutorial, courtesy of the .NET team

This is easier with an example, and thankfully the Microsoft Blazor team's Blazing Pizza's blazor-workshop provides a superb one.

As a quick example from that tutorial - this is the OrderState class that holds the current state relating to the in-progress order:

    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);
        }
    } ```

Note that this state class has no concept of the UI that's bound to it, but it does have properties that control the UI's behaviour.

The razor classes still have the @functions blocks too in that example, but they are considerably simplified by introduce properties in the State class that have explicit roles in controlling the UI behaviour (e.g. ShowingConfigureDialog). For example, from 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> ```

That whole tutorial is excellent, I'd strongly suggest working through it.

But I don't want C# code in my razor files...

You can still put the code from the @code block in the base-class's file and also use the state approach.

The reason people tend not to do that is that if your state file is driving the UI behaviour then the @code wiring code normally ends up as just a few lines, so often it doesn't seem worth putting in a separate file.

like image 38
tomRedox Avatar answered Nov 06 '22 19:11

tomRedox


I have read the article about the parent class approach by creating a class that inherits from ComponentBase and simply inheriting from that base class in your component. I'm not a fan because it forces me to expose class structure that should be maintained internally/privately to the class, and keeping track of protected for inheritance I suppose is the right answer there.

However, I may be missing something here so please don't slaughter me for recommending this, but why can't you just use the partial directive, create a 'sidecar' (my terminology) file of ComponentName.razor.cs and simply declare the class as a partial class. I tried this and it worked fine...

using the current as of this writing template project, in the Counter component, I simply stripped all of the code out to result in the following:

@page "/counter"

<h1>Counter</h1>

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

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

Then I proceeded to create the sidecar file Counter.razor.cs and populated with:

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++;
        }
    }
}

Call me Mr. year 2003, but it works. :)

like image 37
EdFred Avatar answered Nov 06 '22 20:11

EdFred