You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Blazor/Documentation/docusaurus/docs/razor-component/complex-component-main.md

14 KiB

sidebar_position title
6 Component code

Here is the code of our main component, it is he who will call our basic item and perform a large part of the actions.

Creation of the view

<CascadingValue Value="@this">
    <div class="container">
        <div class="row">
            <div class="col-6">
                
                <div>Available items:</div>
                <div>
                    <div class="css-grid">
                        
                        @foreach (var item in Items)
                        {
                            <CraftingItem Item="item" NoDrop="true"/>
                        }
                    </div>
                </div>

            </div>

            <div class="col-6">
                <div>Recipe</div>
                
                <div>
                    
                    <div class="css-recipe">
                        <CraftingItem Index="0"/>
                        <CraftingItem Index="1"/>
                        <CraftingItem Index="2"/>
                        <CraftingItem Index="3"/>
                        <CraftingItem Index="4"/>
                        <CraftingItem Index="5"/>
                        <CraftingItem Index="6"/>
                        <CraftingItem Index="7"/>
                        <CraftingItem Index="8"/>
                    </div>
                </div>
                
                <div>Result</div>
                <div>
                    <CraftingItem Item="RecipeResult"/>
                </div>
            </div>

            <div class="col-12">
                <div>Actions</div>
                <div class="actions" id="actions">

                </div>
            </div>
        </div>
    </div>
</CascadingValue>

Code creation

We use an ObservableCollection to allow an action on a change in the list with the CollectionChanged event.

public partial class Crafting
{
	private Item _recipeResult;

	public Crafting()
	{
		Actions = new ObservableCollection<CraftingAction>();
		Actions.CollectionChanged += OnActionsCollectionChanged;
		this.RecipeItems = new List<Item> { null, null, null, null, null, null, null, null, null };
	}

	public ObservableCollection<CraftingAction> Actions { get; set; }
	public Item CurrentDragItem { get; set; }

	[Parameter]
	public List<Item> Items { get; set; }

	public List<Item> RecipeItems { get; set; }

	public Item RecipeResult
	{
		get => this._recipeResult;
		set
		{
			if (this._recipeResult == value)
			{
				return;
			}

			this._recipeResult = value;
			this.StateHasChanged();
		}
	}

	[Parameter]
	public List<CraftingRecipe> Recipes { get; set; }

	/// <summary>
	/// Gets or sets the java script runtime.
	/// </summary>
	[Inject]
	internal IJSRuntime JavaScriptRuntime { get; set; }

	public void CheckRecipe()
	{
		RecipeResult = null;

		// Get the current model
		var currentModel = string.Join("|", this.RecipeItems.Select(s => s != null ? s.Name : string.Empty));

		this.Actions.Add(new CraftingAction { Action = $"Items : {currentModel}" });

		foreach (var craftingRecipe in Recipes)
		{
			// Get the recipe model
			var recipeModel = string.Join("|", craftingRecipe.Have.SelectMany(s => s));

			this.Actions.Add(new CraftingAction { Action = $"Recipe model : {recipeModel}" });

			if (currentModel == recipeModel)
			{
				RecipeResult = craftingRecipe.Give;
			}
		}
	}

	private void OnActionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
	{
		JavaScriptRuntime.InvokeVoidAsync("Crafting.AddActions", e.NewItems);
	}
}

As for our element we create a CSS file for our component.

.css-grid {
    grid-template-columns: repeat(4,minmax(0,1fr));
    gap: 10px;
    display: grid;
    width: 286px;
}

.css-recipe {
    grid-template-columns: repeat(3,minmax(0,1fr));
    gap: 10px;
    display: grid;
    width: 212px;
}

.actions {
    border: 1px solid black;
    height: 250px;
    overflow: scroll;
}

A JavaScript class is also created to interact with the page.

window.Crafting =
{
    AddActions: function (data) {

        data.forEach(element => {
            var div = document.createElement('div');
            div.innerHTML = 'Action: ' + element.action + ' - Index: ' + element.index;

            if (element.item) {
                div.innerHTML += ' - Item Name: ' + element.item.name;
            }

            document.getElementById('actions').appendChild(div);
        });
    }
}

Concept: CascadingValue & CascadingParameter

In our component we need to know our parent when we are in an item in order to call the CheckRecipe() methods as well as to add actions or to know the element we are moving.

We therefore use in the view of our component Crafting.razor the following code:

<CascadingValue Value="@this">
	...
		<CraftingItem Item="item" NoDrop="true"/>
	...
</CascadingValue>

This code makes it possible to make our component available in all the sub-components found in the CascadingValue tag.

To retrieve our CascadingValue we must use in our component a CascadingParameter as follows:

[CascadingParameter]
public Crafting Parent { get; set; }

It would be possible to go through Parameter but in case of multiple levels, it would be necessary in each level to add our Parameter to nest them:

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

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

<div style="border: 1px solid black; padding: 10px;">
    <strong>MyRootComponent - @Text</strong>
    <div>
        @ChildContent
    </div>
</div>
@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public MyRootComponent RootComponent { get; set; }
}

<div style="border: 1px solid black; padding: 10px;">
    <strong>MyFirstChildComponent - @RootComponent.Text</strong>
    <div>
        @ChildContent
    </div>
</div>
@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public MyRootComponent RootComponent { get; set; }
}

<div style="border: 1px solid black; padding: 10px;">
    <strong>MySecondChildComponent - @RootComponent.Text</strong>
    <div>
        @ChildContent
    </div>
</div>
@code
{
    MyRootComponent MyRootComponent;
}

<MyRootComponent @ref="MyRootComponent" Text="RootComponentText">
    <MyFirstChildComponent RootComponent="@MyRootComponent">
        <MySecondChildComponent RootComponent="@MyRootComponent">
            <div>MySecondChildComponent - Content</div>
        </MySecondChildComponent>
    </MyFirstChildComponent>
</MyRootComponent>

This also forces the developer to declare the component's usage variable each time it is used, as well as having to specify an attribute with its value.

Whereas with CascadingValue & CascadingParameter the code becomes simpler:

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

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

<div style="border: 1px solid black; padding: 10px;">
    <strong>MyRootComponent - @Text</strong>
    <div>
        <CascadingValue Value="@this">
            @ChildContent
        </CascadingValue>
    </div>
</div>
@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [CascadingParameter]
    public MyRootComponent RootComponent { get; set; }
}

<div style="border: 1px solid black; padding: 10px;">
    <strong>MyFirstChildComponent - @RootComponent.Text</strong>
    <div>
        @ChildContent
    </div>
</div>
@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [CascadingParameter]
    public MyRootComponent RootComponent { get; set; }
}

<div style="border: 1px solid black; padding: 10px;">
    <strong>MySecondChildComponent - @RootComponent.Text</strong>
    <div>
        @ChildContent
    </div>
</div>
<MyRootComponent Text="RootComponentText">
    <MyFirstChildComponent>
        <MySecondChildComponent>
            <div>MySecondChildComponent - Content</div>
        </MySecondChildComponent>
    </MyFirstChildComponent>
</MyRootComponent>

Concept: IJSRuntime

To call JS from .net, inject the IJSRuntime abstraction and call one of the following methods:

  • IJSRuntime.InvokeAsync
  • JSRuntimeExtensions.InvokeAsync
  • JSRuntimeExtensions.InvokeVoidAsync

For previous .NET methods that call JS functions:

  • The function identifier ( String ) is relative to the global scope ( window ). To call window.someScope.someFunction, the identifier is someScope.someFunction. There is no need to register the function before it is called.
  • Pass any number of on-Serializable JS arguments in Object[] to a JS function.
  • The cancellation token ( CancellationToken ) propagates a notification that operations should be canceled.
  • TimeSpan represents a time limit for a JS operation.
  • The TValue return type must also be serializable. TValue should be the .NET type that best matches the JS returned type.
  • JS A Promise is returned for InvokeAsync methods. InvokeAsync unwraps Promise and returns the value expected by Promise.

For apps that have Blazor Server pre-render enabled, invoking JS is not possible during the initial pre-render. Interop JS calls should be deferred until the connection with the browser is established.

The following example is based on TextDecoder, a JS-based decoder. The sample demonstrates how to call a JS function from a C# method that offloads a specification from developer code to an existing JS API. The JS function accepts a byte array from a C# method, decodes the array, and returns the text to the component for display.

Add the following JS code inside the closing </body> tag in the wwwroot/index.html file:

<script>
  window.convertArray = (win1251Array) => {
    var win1251decoder = new TextDecoder('windows-1251');
    var bytes = new Uint8Array(win1251Array);
    var decodedArray = win1251decoder.decode(bytes);
    console.log(decodedArray);
    return decodedArray;
  };
</script>

The following CallJsExample1 component:

  • Calls the JS function convertArray with InvokeAsync when selecting a button ( Convert Array ).
  • Once the JS function is called, the passed array is converted to a string. The string is returned to the component for display ( text ).
@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    <a href="https://www.imdb.com/title/tt0379786/" target="_blank">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/" target="_blank">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}

Call JavaScript functions without reading a returned value ( InvokeVoidAsync )

Use InvokeVoidAsync in the following cases:

  • .NET is not required to read the result of a JS call.
  • JS functions return void (0)/void 0 or undefined.

Inside the closing </body> tag of wwwroot/index.html, provide a JS function displayTickerAlert1. The function is called with InvokeVoidAsync and does not return a value:

<script>
  window.displayTickerAlert1 = (symbol, price) => {
    alert(`${symbol}: $${price}!`);
  };
</script>

TickerChanged calls the handleTickerChanged1 method in the following component CallJsExample2.

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string? stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

Calling JavaScript functions and reading a returned value ( InvokeAsync )

Use InvokeAsync when .net needs to read the result of a JS call.

Inside the closing </body> tag of wwwroot/index.html, provide a JS function displayTickerAlert2. The following example returns a string to be displayed by the caller:

<script>
  window.displayTickerAlert2 = (symbol, price) => {
    if (price < 20) {
      alert(`${symbol}: $${price}!`);
      return "User alerted in the browser.";
    } else {
      return "User NOT alerted.";
    }
  };
</script>

TickerChanged calls the handleTickerChanged2 method and displays the returned string in the following component CallJsExample3.

@page "/call-js-example-3"
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string? stockSymbol;
    private decimal price;
    private string? result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}