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.
534 lines
14 KiB
534 lines
14 KiB
![]()
2 years ago
|
---
|
||
|
sidebar_position: 6
|
||
|
title: 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
|
||
|
|
||
|
```cshtml title="Components/Crafting.razor"
|
||
|
<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.
|
||
|
|
||
|
```csharp title="Components/Crafting.razor.cs"
|
||
|
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 title="Components/Crafting.razor.css"
|
||
|
.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.
|
||
|
|
||
|
```js title="Components/Crafting.razor.js"
|
||
|
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:
|
||
|
|
||
|
```html title="Crafting.razor"
|
||
|
<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:
|
||
|
|
||
|
```csharp
|
||
|
[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:
|
||
|
|
||
|
```cshtml title="Components/MyRootComponent.razor"
|
||
|
@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>
|
||
|
```
|
||
|
|
||
|
```cshtml title="Components/MyFirstChildComponent.razor"
|
||
|
@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>
|
||
|
```
|
||
|
|
||
|
```cshtml title="Components/MySecondChildComponent.razor"
|
||
|
@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>
|
||
|
```
|
||
|
|
||
|
```cshtml title="Pages/Index.razor"
|
||
|
@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:
|
||
|
|
||
|
```cshtml title="Components/MyRootComponent.razor"
|
||
|
@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>
|
||
|
```
|
||
|
|
||
|
```cshtml title="Components/MyFirstChildComponent.razor"
|
||
|
@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>
|
||
|
```
|
||
|
|
||
|
```cshtml title="Components/MySecondChildComponent.razor"
|
||
|
@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>
|
||
|
```
|
||
|
|
||
|
```cshtml title="Pages/Index.razor"
|
||
|
<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:
|
||
|
|
||
|
```html
|
||
|
<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` ).
|
||
|
|
||
|
```cshtml title="Pages/CallJsExample1.razor"
|
||
|
@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:
|
||
|
|
||
|
```html
|
||
|
<script>
|
||
|
window.displayTickerAlert1 = (symbol, price) => {
|
||
|
alert(`${symbol}: $${price}!`);
|
||
|
};
|
||
|
</script>
|
||
|
```
|
||
|
|
||
|
`TickerChanged` calls the `handleTickerChanged1` method in the following component `CallJsExample2`.
|
||
|
|
||
|
```cshtml
|
||
|
@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:
|
||
|
|
||
|
```html
|
||
|
<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`.
|
||
|
|
||
|
```cshtml title="Pages/CallJsExample3.razor"
|
||
|
@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}";
|
||
|
}
|
||
|
}
|
||
|
```
|