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

534 lines
14 KiB

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