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 callwindow.someScope.someFunction
, the identifier issomeScope.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 forInvokeAsync
methods.InvokeAsync
unwrapsPromise
and returns the value expected byPromise
.
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
withInvokeAsync
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}";
}
}