Compartir vía


Enlace de datos de ASP.NET Core Blazor

Nota

Esta no es la versión más reciente de este artículo. Para la versión actual, consulta la versión .NET 8 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

En este artículo se explican las características de enlace de datos para componentes Razor y elementos DOM en aplicaciones de Blazor.

Características de los enlaces

Los componentes de Razor proporcionan características de enlace de datos con el atributo de directiva @bindRazor con un valor de campo, propiedad o expresión de Razor.

En el ejemplo siguiente se enlaza:

  • Un valor del elemento <input> al campo inputValue de C#.
  • Un segundo valor del elemento <input> a la propiedad InputValue de C#.

Cuando un elemento <input> pierde el foco, se actualiza su campo o propiedad enlazada.

Bind.razor:

@page "/bind"

<PageTitle>Bind</PageTitle>

<h1>Bind Example</h1>

<p>
    <label>
        inputValue: 
        <input @bind="inputValue" />
    </label>
</p>

<p>
    <label>
        InputValue: 
        <input @bind="InputValue" />
    </label>
</p>

<ul>
    <li><code>inputValue</code>: @inputValue</li>
    <li><code>InputValue</code>: @InputValue</li>
</ul>

@code {
    private string? inputValue;

    private string? InputValue { get; set; }
}
@page "/bind"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <input @bind="InputValue" />
</p>

<ul>
    <li><code>inputValue</code>: @inputValue</li>
    <li><code>InputValue</code>: @InputValue</li>
</ul>

@code {
    private string? inputValue;

    private string? InputValue { get; set; }
}
@page "/bind"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <input @bind="InputValue" />
</p>

<ul>
    <li><code>inputValue</code>: @inputValue</li>
    <li><code>InputValue</code>: @InputValue</li>
</ul>

@code {
    private string? inputValue;

    private string? InputValue { get; set; }
}
@page "/bind"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <input @bind="InputValue" />
</p>

<ul>
    <li><code>inputValue</code>: @inputValue</li>
    <li><code>InputValue</code>: @InputValue</li>
</ul>

@code {
    private string inputValue;

    private string InputValue { get; set; }
}
@page "/bind"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <input @bind="InputValue" />
</p>

<ul>
    <li><code>inputValue</code>: @inputValue</li>
    <li><code>InputValue</code>: @InputValue</li>
</ul>

@code {
    private string inputValue;

    private string InputValue { get; set; }
}

El cuadro de texto se actualiza en la interfaz de usuario solo cuando se representa el componente, no en respuesta al cambio de valor del campo o la propiedad. Como los componentes se representan a sí mismos después de que se ejecute el código del controlador de eventos, las actualizaciones de campos y propiedades normalmente se reflejan en la interfaz de usuario justo después de que se desencadene un controlador de eventos.

Como demostración de cómo el enlace de datos se compone en HTML, en el ejemplo siguiente se enlaza la propiedad InputValue al segundo elemento de <input>value y atributos de onchange (change). El segundo elemento <input> del ejemplo siguiente es una demostración de concepto y no está pensado para sugerir cómo debe enlazar los datos en los componentes de Razor .

BindTheory.razor:

@page "/bind-theory"

<PageTitle>Bind Theory</PageTitle>

<h1>Bind Theory Example</h1>

<p>
    <label>
        Normal Blazor binding: 
        <input @bind="InputValue" />
    </label>
</p>

<p>
    <label>
        Demonstration of equivalent HTML binding: 
        <input value="@InputValue" @onchange="@((ChangeEventArgs __e) =>
            InputValue = __e?.Value?.ToString())" />
    </label>
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
@page "/bind-theory"

<p>
    <label>
        Normal Blazor binding: 
        <input @bind="InputValue" />
    </label>
</p>

<p>
    <label>
        Demonstration of equivalent HTML binding: 
        <input value="@InputValue"
            @onchange="@((ChangeEventArgs __e) => InputValue = __e?.Value?.ToString())" />
    </label>
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
@page "/bind-theory"

<p>
    <label>
        Normal Blazor binding: 
        <input @bind="InputValue" />
    </label>
</p>

<p>
    <label>
        Demonstration of equivalent HTML binding: 
        <input value="@InputValue"
            @onchange="@((ChangeEventArgs __e) => InputValue = __e?.Value?.ToString())" />
    </label>
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
@page "/bind-theory"

<p>
    <label>
        Normal Blazor binding: 
        <input @bind="InputValue" />
    </label>
</p>

<p>
    <label>
        Demonstration of equivalent HTML binding: 
        <input value="@InputValue"
            @onchange="@((ChangeEventArgs __e) => InputValue = __e.Value.ToString())" />
    </label>
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string InputValue { get; set; }
}
@page "/bind-theory"

<p>
    <label>
        Normal Blazor binding: 
        <input @bind="InputValue" />
    </label>
</p>

<p>
    <label>
        Demonstration of equivalent HTML binding: 
        <input value="@InputValue"
            @onchange="@((ChangeEventArgs __e) => InputValue = __e.Value.ToString())" />
    </label>
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string InputValue { get; set; }
}

Cuando se representa el componente BindTheory, el valor value del elemento <input> de demostración HTML procede de la propiedad InputValue. Cuando el usuario especifica un valor en el cuadro de texto y cambia el foco del elemento, se desencadena el evento onchange y la propiedad InputValue se establece en el valor modificado. En realidad, la generación de código es más compleja, porque @bind administra los casos en los que se realizan conversiones de tipos. En general, @bind asocia el valor actual de una expresión a un atributo value de <input> y controla los cambios mediante el controlador registrado.

Enlaza una propiedad o un campo en otros eventos de DOM mediante la inclusión de un atributo @bind:event="{EVENT}" con un evento DOM para el marcador de posición {EVENT}. En el ejemplo siguiente se enlaza la propiedad InputValue al valor del elemento <input> cuando se desencadena el evento oninput del elemento (input). A diferencia del evento onchange (change), que se desencadena cuando el elemento pierde el foco, oninput (input) se activa cuando cambia el valor del cuadro de texto.

Page/BindEvent.razor:

@page "/bind-event"

<PageTitle>Bind Event</PageTitle>

<h1>Bind Event Example</h1>

<p>
    <label>
        InputValue: 
        <input @bind="InputValue" @bind:event="oninput" />
    </label>
    
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
@page "/bind-event"

<p>
    <input @bind="InputValue" @bind:event="oninput" />
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
@page "/bind-event"

<p>
    <input @bind="InputValue" @bind:event="oninput" />
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
@page "/bind-event"

<p>
    <input @bind="InputValue" @bind:event="oninput" />
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string InputValue { get; set; }
}
@page "/bind-event"

<p>
    <input @bind="InputValue" @bind:event="oninput" />
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string InputValue { get; set; }
}

El enlace de atributos de Razor distingue mayúsculas de minúsculas:

  • @bind y @bind:event son válidos.
  • @Bind/@Bind:Event (letras mayúsculas B y E) o @BIND/@BIND:EVENT (todas las letras mayúsculas) no son válidos.

Para ejecutar lógica asincrónica después del enlace, use @bind:after="{EVENT}" con un evento DOM para el marcador de posición {EVENT}. Un método de C# asignado no se ejecuta hasta que el valor enlazado se asigna de forma sincrónica.

No se admite el uso de un parámetro de devolución de llamada de eventos(EventCallback/EventCallback<T>) con @bind:after. En su lugar, pasa un método que devuelva Action o Task a @bind:after.

En el ejemplo siguiente:

  • Cada value del elemento <input> está enlazado al campo searchText de forma sincrónica.
  • El método PerformSearch se ejecuta de forma asincrónica:
    • Cuando el primer cuadro pierde el foco (evento onchange) después de cambiar el valor.
    • Después de cada pulsación de tecla (evento oninput) en el segundo cuadro.
  • PerformSearch llama a un servicio con un método asincrónico (FetchAsync) para devolver los resultados de la búsqueda.
@inject ISearchService SearchService

<input @bind="searchText" @bind:after="PerformSearch" />
<input @bind="searchText" @bind:event="oninput" @bind:after="PerformSearch" />

@code {
    private string? searchText;
    private string[]? searchResult;

    private async Task PerformSearch() => 
        searchResult = await SearchService.FetchAsync(searchText);
}

Ejemplos adicionales

BindAfter.razor:

@page "/bind-after"
@using Microsoft.AspNetCore.Components.Forms

<h1>Bind After Examples</h1>

<h2>Elements</h2>

<input type="text" @bind="text" @bind:after="() => { }" />

<input type="text" @bind="text" @bind:after="After" />

<input type="text" @bind="text" @bind:after="AfterAsync" />

<h2>Components</h2>

<InputText @bind-Value="text" @bind-Value:after="() => { }" />

<InputText @bind-Value="text" @bind-Value:after="After" />

<InputText @bind-Value="text" @bind-Value:after="AfterAsync" />

@code {
    private string text = "";

    private void After() {}
    private Task AfterAsync() { return Task.CompletedTask; }
}

Para obtener más información sobre el componente InputText, consulta Formularios y componentes de entrada de ASP.NET Core Blazor.

Los componentes admiten el enlace de datos bidireccional mediante la definición de un par de parámetros:

  • @bind:get: especifica el valor que se va a enlazar.
  • @bind:set: especifica una devolución de llamada para cuando cambia el valor.

Los modificadores @bind:get y @bind:set siempre se usan juntos.

Ejemplos

BindGetSet.razor:

@page "/bind-get-set"
@using Microsoft.AspNetCore.Components.Forms

<h1>Bind Get Set Examples</h1>

<h2>Elements</h2>

<input type="text" @bind:get="text" @bind:set="(value) => { text = value; }" />
<input type="text" @bind:get="text" @bind:set="Set" />
<input type="text" @bind:get="text" @bind:set="SetAsync" />

<h2>Components</h2>

<InputText @bind-Value:get="text" @bind-Value:set="(value) => { text = value; }" />
<InputText @bind-Value:get="text" @bind-Value:set="Set" />
<InputText @bind-Value:get="text" @bind-Value:set="SetAsync" />

@code {
    private string text = "";

    private void Set(string value)
    {
        text = value;
    }

    private Task SetAsync(string value)
    {
        text = value;
        return Task.CompletedTask;
    }
}

Para obtener más información sobre el componente InputText, consulta Formularios y componentes de entrada de ASP.NET Core Blazor.

Para obtener otro ejemplo de uso de @bind:get y @bind:set, consulta la sección Enlace entre más de dos componentes más adelante en este artículo.

El enlace de atributos de Razor distingue mayúsculas de minúsculas:

  • @bind, @bind:eventy @bind:after son válidos.
  • @Bind/@bind:Event/@bind:aftEr (letras mayúsculas) o @BIND/@BIND:EVENT/@BIND:AFTER (todo en letras mayúsculas) no son válidas.

Uso de modificadores @bind:get/@bind:set y evitar controladores de eventos para el enlace de datos bidireccional

El enlace de datos bidireccional no se puede implementar con un controlador de eventos. Usa los modificadores @bind:get/@bind:set para el enlace de datos bidireccional.

Ten en cuenta el siguiente enfoque disfuncional para el enlace de datos bidireccional mediante un controlador de eventos:

<p>
    <input value="@inputValue" @oninput="OnInput" />
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private string? inputValue;

    private void OnInput(ChangeEventArgs args)
    {
        var newValue = args.Value?.ToString() ?? string.Empty;

        inputValue = newValue.Length > 4 ? "Long!" : newValue;
    }
}

El controlador de eventos OnInput actualiza el valor de inputValue a Long! después de proporcionar un cuarto carácter. Sin embargo, el usuario puede seguir agregando caracteres al valor de elemento en UI. El valor de inputValue no se enlaza al valor del elemento con cada pulsación de tecla. El ejemplo anterior solo es capaz de enlazar datos de forma unidireccional.

El motivo de este comportamiento es que Blazor no es consciente de que el código pretende modificar el valor de inputValue en el controlador de eventos. Blazor no intenta exigir que los valores de elemento de DOM y los valores de variable de .NET coincidan a menos que estén enlazados con la sintaxis @bind. En versiones anteriores de Blazor, el enlace de datos bidireccional se implementaba enlazando el elemento a una propiedad y controlando el valor de la propiedad con su establecedor. En ASP.NET Core en .NET 7 o posterior, se usa la sintaxis del modificador @bind:get/@bind:set para implementar el enlace de datos bidireccional, como se muestra en el ejemplo siguiente.

Ten en cuenta el siguiente enfoque correcto mediante @bind:get/@bind:set para el enlace de datos bidireccional:

<p>
    <input @bind:event="oninput" @bind:get="inputValue" @bind:set="OnInput" />
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private string? inputValue;

    private void OnInput(string value)
    {
        var newValue = value ?? string.Empty;

        inputValue = newValue.Length > 4 ? "Long!" : newValue;
    }
}

El uso de los modificadores @bind:get/@bind:set controla el valor subyacente de inputValue a través de @bind:set y enlaza el valor de inputValue al valor del elemento a través de @bind:get. En el ejemplo anterior se muestra el enfoque correcto para implementar el enlace de datos bidireccional.

Enlace a una propiedad con los descriptores de acceso get y set de C#

Los descriptores de acceso get y set de C# se pueden usar para crear un comportamiento de formato de enlace personalizado, como muestra el siguiente componente DecimalBinding. El componente enlaza un decimal positivo o negativo con hasta tres posiciones decimales a un elemento <input> por medio de una propiedad string (DecimalValue).

DecimalBinding.razor:

@page "/decimal-binding"
@using System.Globalization

<PageTitle>Decimal Binding</PageTitle>

<h1>Decimal Binding Example</h1>

<p>
    <label>
        Decimal value (±0.000 format):
        <input @bind="DecimalValue" />
    </label>
</p>

<p>
    <code>decimalValue</code>: @decimalValue
</p>

@code {
    private decimal decimalValue = 1.1M;
    private NumberStyles style = 
        NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
    private CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    private string DecimalValue
    {
        get => decimalValue.ToString("0.000", culture);
        set
        {
            if (Decimal.TryParse(value, style, culture, out var number))
            {
                decimalValue = Math.Round(number, 3);
            }
        }
    }
}
@page "/decimal-binding"
@using System.Globalization

<p>
    <label>
        Decimal value (±0.000 format):
        <input @bind="DecimalValue" />
    </label>
</p>

<p>
    <code>decimalValue</code>: @decimalValue
</p>

@code {
    private decimal decimalValue = 1.1M;
    private NumberStyles style = 
        NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
    private CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    private string DecimalValue
    {
        get => decimalValue.ToString("0.000", culture);
        set
        {
            if (Decimal.TryParse(value, style, culture, out var number))
            {
                decimalValue = Math.Round(number, 3);
            }
        }
    }
}
@page "/decimal-binding"
@using System.Globalization

<p>
    <label>
        Decimal value (±0.000 format):
        <input @bind="DecimalValue" />
    </label>
</p>

<p>
    <code>decimalValue</code>: @decimalValue
</p>

@code {
    private decimal decimalValue = 1.1M;
    private NumberStyles style = 
        NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
    private CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    private string DecimalValue
    {
        get => decimalValue.ToString("0.000", culture);
        set
        {
            if (Decimal.TryParse(value, style, culture, out var number))
            {
                decimalValue = Math.Round(number, 3);
            }
        }
    }
}
@page "/decimal-binding"
@using System.Globalization

<p>
    <label>
        Decimal value (±0.000 format):
        <input @bind="DecimalValue" />
    </label>
</p>

<p>
    <code>decimalValue</code>: @decimalValue
</p>

@code {
    private decimal decimalValue = 1.1M;
    private NumberStyles style = 
        NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
    private CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    private string DecimalValue
    {
        get => decimalValue.ToString("0.000", culture);
        set
        {
            if (Decimal.TryParse(value, style, culture, out var number))
            {
                decimalValue = Math.Round(number, 3);
            }
        }
    }
}
@page "/decimal-binding"
@using System.Globalization

<p>
    <label>
        Decimal value (±0.000 format):
        <input @bind="DecimalValue" />
    </label>
</p>

<p>
    <code>decimalValue</code>: @decimalValue
</p>

@code {
    private decimal decimalValue = 1.1M;
    private NumberStyles style = 
        NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
    private CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    private string DecimalValue
    {
        get => decimalValue.ToString("0.000", culture);
        set
        {
            if (Decimal.TryParse(value, style, culture, out var number))
            {
                decimalValue = Math.Round(number, 3);
            }
        }
    }
}

Nota

El enlace bidireccional a una propiedad con descriptores de acceso get/set requiere descartar la clase Task devuelta por EventCallback.InvokeAsync. Para el enlace de datos bidireccional, se recomienda usar los modificadores @bind:get/@bind:set. Para obtener más información,consulta la guía sobre @bind:get/@bind:set más arriba en este mismo artículo.

Nota

El enlace bidireccional a una propiedad con descriptores de acceso get/set requiere descartar la clase Task devuelta por EventCallback.InvokeAsync. Para el enlace de datos bidireccional en ASP.NET Core en .NET 7 o posterior, se recomienda usar los modificadores @bind:get/@bind:set, que se describen en la versión 7.0 o versiones posteriores de este artículo.

Selección de varias opciones con elementos <select>

El enlace admite la selección de la opción multiple con los elementos <select>. El evento @onchange proporciona una matriz de los elementos seleccionados mediante argumentos de evento (ChangeEventArgs). El valor se debe enlazar a un tipo de matriz.

BindMultipleInput.razor:

@page "/bind-multiple-input"

<h1>Bind Multiple <code>input</code>Example</h1>

<p>
    <label>
        Select one or more cars: 
        <select @onchange="SelectedCarsChanged" multiple>
            <option value="audi">Audi</option>
            <option value="jeep">Jeep</option>
            <option value="opel">Opel</option>
            <option value="saab">Saab</option>
            <option value="volvo">Volvo</option>
        </select>
    </label>
</p>

<p>
    Selected Cars: @string.Join(", ", SelectedCars)
</p>

<p>
    <label>
        Select one or more cities: 
        <select @bind="SelectedCities" multiple>
            <option value="bal">Baltimore</option>
            <option value="la">Los Angeles</option>
            <option value="pdx">Portland</option>
            <option value="sf">San Francisco</option>
            <option value="sea">Seattle</option>
        </select>
    </label>
</p>

<span>
    Selected Cities: @string.Join(", ", SelectedCities)
</span>

@code {
    public string[] SelectedCars { get; set; } = new string[] { };
    public string[] SelectedCities { get; set; } = new[] { "bal", "sea" };

    private void SelectedCarsChanged(ChangeEventArgs e)
    {
        if (e.Value is not null)
        {
            SelectedCars = (string[])e.Value;
        }
    }
}

Para obtener información sobre cómo se controlan las cadenas vacías y los valores null en el enlace de datos, consulta la sección Enlace de opciones del elemento <select> a valores null de objetos de C#.

Enlace de opciones del elemento <select> a valores null de un objeto C#

No hay ninguna manera razonable de representar un valor de opción de elemento <select> como valor null de objeto C#, por los siguientes motivos:

  • Los atributos HTML no pueden tener valores null. El equivalente más cercano a null en HTML es la ausencia del atributo value de HTML del elemento <option>.
  • Al seleccionar un <option> sin ningún atributo value, el explorador trata el valor como contenido de texto de ese elemento de <option>.

El marco de Blazor no intenta suprimir el comportamiento predeterminado porque implicaría lo siguiente:

  • Crear una cadena de soluciones alternativas de casos especiales en el marco.
  • Hacer cambios importantes en el comportamiento actual del marco.

El null más plausible equivalente en HTML es una cadena vacía value. El marco Blazor controla null en las conversiones de cadenas vacías para el enlace bidireccional al valor de <select>.

Valores no analizables

Cuando un usuario proporciona un valor no analizable a un elemento enlazado a datos, el valor no analizable se revierte automáticamente a su valor anterior cuando se desencadena el evento de enlace.

Considera el siguiente componente, donde un elemento <input> se enlaza a un tipo int con un valor inicial de 123.

UnparsableValues.razor:

@page "/unparsable-values"

<PageTitle>Unparsable Values</PageTitle>

<h1>Unparsable Values Example</h1>

<p>
    <label>
        inputValue: 
        <input @bind="inputValue" />
    </label>
    
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private int inputValue = 123;
}
@page "/unparseable-values"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private int inputValue = 123;
}
@page "/unparseable-values"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private int inputValue = 123;
}
@page "/unparseable-values"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private int inputValue = 123;
}
@page "/unparseable-values"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private int inputValue = 123;
}

El enlace se aplica al evento onchange del elemento. Si el usuario actualiza el valor de la entrada del cuadro de texto a 123.45 y cambia el foco, el valor del elemento se revierte a 123 cuando onchange se activa. Cuando el valor 123.45 se rechaza en favor del valor original de 123, el usuario entiende que no se ha aceptado su valor.

En el caso del evento oninput (@bind:event="oninput"), la reversión del valor se produce después de cualquier pulsación de tecla que especifique un valor no analizable. Cuando el destino es el evento oninput con un tipo enlazado a int, se impide que el usuario escriba el carácter de punto (.). El carácter de punto (.) se elimina de forma inmediata, por lo que el usuario recibe un comentario al instante informándole de que solo se permiten números enteros. Hay escenarios en los que la reversión del valor del evento oninput no es ideal, por ejemplo, cuando se debe permitir que el usuario borre un valor <input> que no se puede analizar. De forma alternativa, puedes hacer lo siguiente:

  • No uses el evento oninput. Usa el evento onchange predeterminado para que no se revierta un valor no válido hasta que el elemento pierda el foco.
  • Establece un enlace a un tipo que acepte valores NULL, como int? o string y usar los modificadores @bind:get/@bind:set (descritos anteriormente en este artículo) o establecer un enlace a una propiedad con lógica personalizada de descriptores de acceso get y set para controlar entradas no válidas.
  • Usa un componente de entrada, como InputNumber<TValue> o InputDate<TValue>, con validación de formularios. Los componentes de entrada junto con los componentes de validación de formularios proporcionan compatibilidad integrada para administrar entradas no válidas:
    • Permiten que el usuario proporcione entradas no válidas y reciba errores de validación en la clase EditContext asociada.
    • Muestran errores de validación en la interfaz de usuario sin impedir que el usuario escriba datos adicionales en el formulario web.

Cadenas de formato

El enlace de datos funciona con una única cadena de formato DateTime mediante @bind:format="{FORMAT STRING}", donde el marcador de posición {FORMAT STRING} es la cadena de formato. Otras expresiones de formato, como los formatos de moneda o número, no están disponibles en este momento, pero podrían agregarse en una versión futura.

DateBinding.razor:

@page "/date-binding"

<PageTitle>Date Binding</PageTitle>

<h1>Date Binding Example</h1>

<p>
    <label>
        <code>yyyy-MM-dd</code> format:
        <input @bind="startDate" @bind:format="yyyy-MM-dd" />
    </label>
</p>

<p>
    <code>startDate</code>: @startDate
</p>

@code {
    private DateTime startDate = new(2020, 1, 1);
}
@page "/date-binding"

<p>
    <label>
        <code>yyyy-MM-dd</code> format:
        <input @bind="startDate" @bind:format="yyyy-MM-dd" />
    </label>
</p>

<p>
    <code>startDate</code>: @startDate
</p>

@code {
    private DateTime startDate = new(2020, 1, 1);
}
@page "/date-binding"

<p>
    <label>
        <code>yyyy-MM-dd</code> format:
        <input @bind="startDate" @bind:format="yyyy-MM-dd" />
    </label>
</p>

<p>
    <code>startDate</code>: @startDate
</p>

@code {
    private DateTime startDate = new(2020, 1, 1);
}
@page "/date-binding"

<p>
    <label>
        <code>yyyy-MM-dd</code> format:
        <input @bind="startDate" @bind:format="yyyy-MM-dd" />
    </label>
</p>

<p>
    <code>startDate</code>: @startDate
</p>

@code {
    private DateTime startDate = new(2020, 1, 1);
}
@page "/date-binding"

<p>
    <label>
        <code>yyyy-MM-dd</code> format:
        <input @bind="startDate" @bind:format="yyyy-MM-dd" />
    </label>
</p>

<p>
    <code>startDate</code>: @startDate
</p>

@code {
    private DateTime startDate = new DateTime(2020, 1, 1);
}

En el código anterior, el tipo de campo (atributo <input>) del elemento type tiene como valor predeterminado text.

Se aceptan los valores NULL System.DateTime y System.DateTimeOffset:

private DateTime? date;
private DateTimeOffset? dateOffset;

No se recomienda especificar un formato para el tipo de campo date porque Blazor tiene compatibilidad integrada para dar formato a las fechas. A pesar de la recomendación, usa solo el formato de fecha yyyy-MM-dd para que el enlace funcione correctamente si se proporciona un formato con el tipo de campo date:

<input type="date" @bind="startDate" @bind:format="yyyy-MM-dd">

Enlace con parámetros de componente

Un escenario habitual es enlazar una propiedad de un componente secundario a una propiedad de su elemento primario. Este escenario se denomina enlace encadenado porque se producen varios niveles de enlace simultáneamente.

No se pueden implementar enlaces encadenados con la sintaxis @bind en el componente secundario. Es necesario especificar un controlador de eventos y un valor por separado para permitir la actualización de la propiedad en el elemento primario desde el componente secundario. El componente primario sigue aprovechando la sintaxis @bind para configurar el enlace de datos con el componente secundario.

El siguiente componente ChildBind tiene un parámetro de componente Year y un elemento EventCallback<TValue>. Por convención, el elemento EventCallback<TValue> del parámetro se debe denominar como el nombre de parámetro del componente con el sufijo "Changed". La sintaxis de nomenclatura es {PARAMETER NAME}Changed, donde el marcador de posición {PARAMETER NAME} es el nombre del parámetro. En el ejemplo siguiente, el elemento EventCallback<TValue> se denomina YearChanged.

EventCallback.InvokeAsync invoca al delegado asociado al enlace con el argumento proporcionado y envía una notificación de evento de la propiedad modificada.

ChildBind.razor:

<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild() => 
        await YearChanged.InvokeAsync(Random.Shared.Next(1950, 2021));
}
<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild()
    {
        await YearChanged.InvokeAsync(Random.Shared.Next(1950, 2021));
    }
}
<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild()
    {
        await YearChanged.InvokeAsync(Random.Shared.Next(1950, 2021));
    }
}
<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    private Random r = new();

    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild()
    {
        await YearChanged.InvokeAsync(r.Next(1950, 2021));
    }
}
<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    private Random r = new Random();

    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild()
    {
        await YearChanged.InvokeAsync(r.Next(1950, 2021));
    }
}

Para obtener más información sobre los eventos y EventCallback<TValue>, consulta la sección EventCallback del artículo Control de eventos de Blazor en ASP.NET Core.

En el siguiente componente Parent1, el campo year se enlaza al parámetro Year del componente secundario. El parámetro Year se puede enlazar porque tiene un evento YearChanged complementario que coincide con el tipo del parámetro Year.

Parent1.razor:

@page "/parent-1"

<PageTitle>Parent 1</PageTitle>

<h1>Parent Example 1</h1>

<p>Parent <code>year</code>: @year</p>

<button @onclick="UpdateYear">Update Parent <code>year</code></button>

<ChildBind @bind-Year="year" />

@code {
    private int year = 1979;

    private void UpdateYear() => year = Random.Shared.Next(1950, 2021);
}
@page "/parent-1"

<h1>Parent Component</h1>

<p>Parent <code>year</code>: @year</p>

<button @onclick="UpdateYear">Update Parent <code>year</code></button>

<ChildBind @bind-Year="year" />

@code {
    private int year = 1979;

    private void UpdateYear()
    {
        year = Random.Shared.Next(1950, 2021);
    }
}
@page "/parent-1"

<h1>Parent Component</h1>

<p>Parent <code>year</code>: @year</p>

<button @onclick="UpdateYear">Update Parent <code>year</code></button>

<ChildBind @bind-Year="year" />

@code {
    private int year = 1979;

    private void UpdateYear()
    {
        year = Random.Shared.Next(1950, 2021);
    }
}
@page "/parent-1"

<h1>Parent Component</h1>

<p>Parent <code>year</code>: @year</p>

<button @onclick="UpdateYear">Update Parent <code>year</code></button>

<ChildBind @bind-Year="year" />

@code {
    private Random r = new();
    private int year = 1979;

    private void UpdateYear()
    {
        year = r.Next(1950, 2021);
    }
}
@page "/parent-1"

<h1>Parent Component</h1>

<p>Parent <code>year</code>: @year</p>

<button @onclick="UpdateYear">Update Parent <code>year</code></button>

<ChildBind @bind-Year="year" />

@code {
    private Random r = new Random();
    private int year = 1979;

    private void UpdateYear()
    {
        year = r.Next(1950, 2021);
    }
}

El enlace de parámetros de componente también puede desencadenar eventos @bind:after. En el ejemplo siguiente, el método YearUpdated se ejecuta asincrónicamente después de enlazar el parámetro de componente Year.

<ChildBind @bind-Year="year" @bind-Year:after="YearUpdated" />

@code {
    ...

    private async Task YearUpdated()
    {
        ... = await ...;
    }
}

Por convención, una propiedad se puede enlazar a un controlador de eventos correspondiente si se incluye un atributo @bind-{PROPERTY}:event asignado al controlador, donde el marcador de posición {PROPERTY} es la propiedad. <ChildBind @bind-Year="year" /> equivale a escribir lo siguiente:

<ChildBind @bind-Year="year" @bind-Year:event="YearChanged" />

En un ejemplo real más sofisticado, el siguiente componente PasswordEntry:

  • Establece el valor del elemento <input> en un campo password.
  • Expone los cambios de una propiedad Password a un componente primario con un objeto EventCallback que pasa el valor actual del campo password del elemento secundario como su argumento.
  • Usa el evento onclick para desencadenar el método ToggleShowPassword. Para obtener más información, consulta Control de eventos de Blazor en ASP.NET Core.

PasswordEntry.razor:

<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged"
                       required
                       type="@(showPassword ? "text" : "password")"
                       value="@password" />
            </label>
        </p>
        <button class="btn btn-primary" @onclick="ToggleShowPassword">
            Show password
        </button>
    </div>
</div>

@code {
    private bool showPassword;
    private string? password;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private async Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        await PasswordChanged.InvokeAsync(password);
    }

    private void ToggleShowPassword() => showPassword = !showPassword;
}
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged"
                       required
                       type="@(showPassword ? "text" : "password")"
                       value="@password" />
            </label>
        </p>
        <button class="btn btn-primary" @onclick="ToggleShowPassword">
            Show password
        </button>
    </div>
</div>

@code {
    private bool showPassword;
    private string? password;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private async Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        await PasswordChanged.InvokeAsync(password);
    }

    private void ToggleShowPassword()
    {
        showPassword = !showPassword;
    }
}
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged"
                       required
                       type="@(showPassword ? "text" : "password")"
                       value="@password" />
            </label>
        </p>
        <button class="btn btn-primary" @onclick="ToggleShowPassword">
            Show password
        </button>
    </div>
</div>

@code {
    private bool showPassword;
    private string? password;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private async Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        await PasswordChanged.InvokeAsync(password);
    }

    private void ToggleShowPassword()
    {
        showPassword = !showPassword;
    }
}
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged"
                       required
                       type="@(showPassword ? "text" : "password")"
                       value="@password" />
            </label>
        </p>
        <button class="btn btn-primary" @onclick="ToggleShowPassword">
            Show password
        </button>
    </div>
</div>

@code {
    private bool showPassword;
    private string password;

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

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private async Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e.Value.ToString();

        await PasswordChanged.InvokeAsync(password);
    }

    private void ToggleShowPassword()
    {
        showPassword = !showPassword;
    }
}
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged"
                       required
                       type="@(showPassword ? "text" : "password")"
                       value="@password" />
            </label>
        </p>
        <button class="btn btn-primary" @onclick="ToggleShowPassword">
            Show password
        </button>
    </div>
</div>

@code {
    private bool showPassword;
    private string password;

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

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private async Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e.Value.ToString();

        await PasswordChanged.InvokeAsync(password);
    }

    private void ToggleShowPassword()
    {
        showPassword = !showPassword;
    }
}

El componente PasswordEntry se usa en otro componente, como el siguiente ejemplo de componente PasswordBinding.

PasswordBinding.razor:

@page "/password-binding"

<PageTitle>Password Binding</PageTitle>

<h1>Password Binding Example</h1>

<PasswordEntry @bind-Password="password" />

<p>
    <code>password</code>: @password
</p>

@code {
    private string password = "Not set";
}
@page "/password-binding"

<h1>Password Binding</h1>

<PasswordEntry @bind-Password="password" />

<p>
    <code>password</code>: @password
</p>

@code {
    private string password = "Not set";
}
@page "/password-binding"

<h1>Password Binding</h1>

<PasswordEntry @bind-Password="password" />

<p>
    <code>password</code>: @password
</p>

@code {
    private string password = "Not set";
}
@page "/password-binding"

<h1>Password Binding</h1>

<PasswordEntry @bind-Password="password" />

<p>
    <code>password</code>: @password
</p>

@code {
    private string password = "Not set";
}
@page "/password-binding"

<h1>Password Binding</h1>

<PasswordEntry @bind-Password="password" />

<p>
    <code>password</code>: @password
</p>

@code {
    private string password = "Not set";
}

Cuando el componente PasswordBinding se representa inicialmente, el valor password de Not set se muestra en la interfaz de usuario. Después de la representación inicial, el valor de password refleja los cambios realizados en el valor del parámetro de componente Password del componente PasswordEntry.

Nota

En el ejemplo anterior se enlaza la contraseña de forma unidireccional desde el componente PasswordEntry secundario al componente PasswordBinding primario. El enlace bidireccional no es un requisito en este escenario si el objetivo es que la aplicación tenga un componente de entrada de contraseña compartida para reutilizarlo en la aplicación que simplemente pasa la contraseña al elemento primario. Para obtener un enfoque que permita el enlace bidireccional sin escribir directamente en el parámetro del componente secundario, consulta el ejemplo del componente NestedChild en la sección Enlace entre más de dos componentes de este artículo.

Realiza las comprobaciones o detecta los errores en el controlador. El siguiente componente PasswordEntry revisado informa de inmediato al usuario si se emplea un espacio en el valor de la contraseña.

PasswordEntry.razor:

<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged"
                       required
                       type="@(showPassword ? "text" : "password")"
                       value="@password" />
            </label>
            <span class="text-danger">@validationMessage</span>
        </p>
        <button class="btn btn-primary" @onclick="ToggleShowPassword">
            Show password
        </button>
    </div>
</div>

@code {
    private bool showPassword;
    private string? password;
    private string? validationMessage;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        if (password != null && password.Contains(' '))
        {
            validationMessage = "Spaces not allowed!";

            return Task.CompletedTask;
        }
        else
        {
            validationMessage = string.Empty;

            return PasswordChanged.InvokeAsync(password);
        }
    }

    private void ToggleShowPassword() => showPassword = !showPassword;
}
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged"
                       required
                       type="@(showPassword ? "text" : "password")"
                       value="@password" />
            </label>
            <span class="text-danger">@validationMessage</span>
        </p>
        <button class="btn btn-primary" @onclick="ToggleShowPassword">
            Show password
        </button>
    </div>
</div>

@code {
    private bool showPassword;
    private string? password;
    private string? validationMessage;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        if (password != null && password.Contains(' '))
        {
            validationMessage = "Spaces not allowed!";

            return Task.CompletedTask;
        }
        else
        {
            validationMessage = string.Empty;

            return PasswordChanged.InvokeAsync(password);
        }
    }

    private void ToggleShowPassword()
    {
        showPassword = !showPassword;
    }
}

En el ejemplo siguiente, el método PasswordUpdated se ejecuta asincrónicamente después de enlazar el parámetro de componente Password:

<PasswordEntry @bind-Password="password" @bind-Password:after="PasswordUpdated" />
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged"
                       required
                       type="@(showPassword ? "text" : "password")"
                       value="@password" />
            </label>
            <span class="text-danger">@validationMessage</span>
        </p>
        <button class="btn btn-primary" @onclick="ToggleShowPassword">
            Show password
        </button>
    </div>
</div>

@code {
    private bool showPassword;
    private string? password;
    private string? validationMessage;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        if (password != null && password.Contains(' '))
        {
            validationMessage = "Spaces not allowed!";

            return Task.CompletedTask;
        }
        else
        {
            validationMessage = string.Empty;

            return PasswordChanged.InvokeAsync(password);
        }
    }

    private void ToggleShowPassword()
    {
        showPassword = !showPassword;
    }
}
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged"
                       required
                       type="@(showPassword ? "text" : "password")"
                       value="@password" />
            </label>
            <span class="text-danger">@validationMessage</span>
        </p>
        <button class="btn btn-primary" @onclick="ToggleShowPassword">
            Show password
        </button>
    </div>
</div>

@code {
    private bool showPassword;
    private string password;
    private string validationMessage;

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

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e.Value.ToString();

        if (password.Contains(' '))
        {
            validationMessage = "Spaces not allowed!";

            return Task.CompletedTask;
        }
        else
        {
            validationMessage = string.Empty;

            return PasswordChanged.InvokeAsync(password);
        }
    }

    private void ToggleShowPassword()
    {
        showPassword = !showPassword;
    }
}
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged"
                       required
                       type="@(showPassword ? "text" : "password")"
                       value="@password" />
            </label>
            <span class="text-danger">@validationMessage</span>
        </p>
        <button class="btn btn-primary" @onclick="ToggleShowPassword">
            Show password
        </button>
    </div>
</div>

@code {
    private bool showPassword;
    private string password;
    private string validationMessage;

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

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e.Value.ToString();

        if (password.Contains(' '))
        {
            validationMessage = "Spaces not allowed!";

            return Task.CompletedTask;
        }
        else
        {
            validationMessage = string.Empty;

            return PasswordChanged.InvokeAsync(password);
        }
    }

    private void ToggleShowPassword()
    {
        showPassword = !showPassword;
    }
}

Enlace entre más de dos componentes

Puedes enlazar parámetros a través de cualquier número de componentes anidados, pero debes respetar el flujo unidireccional de los datos:

  • Las notificaciones de cambio suben en la jerarquía.
  • Los valores nuevos de parámetro bajan en la jerarquía.

Un enfoque común y recomendado es almacenar solo los datos subyacentes en el componente primario para evitar la confusión sobre el estado que se debe actualizar, como se muestra en el ejemplo siguiente.

Parent2.razor:

@page "/parent-2"

<PageTitle>Parent 2</PageTitle>

<h1>Parent Example 2</h1>

<p>Parent Message: <b>@parentMessage</b></p>

<p>
    <button @onclick="ChangeValue">Change from Parent</button>
</p>

<NestedChild @bind-ChildMessage="parentMessage" />

@code {
    private string parentMessage = "Initial value set in Parent";

    private void ChangeValue() => parentMessage = $"Set in Parent {DateTime.Now}";
}
@page "/parent-2"

<h1>Parent Component</h1>

<p>Parent Message: <b>@parentMessage</b></p>

<p>
    <button @onclick="ChangeValue">Change from Parent</button>
</p>

<NestedChild @bind-ChildMessage="parentMessage" />

@code {
    private string parentMessage = "Initial value set in Parent";

    private void ChangeValue()
    {
        parentMessage = $"Set in Parent {DateTime.Now}";
    }
}
@page "/parent-2"

<h1>Parent Component</h1>

<p>Parent Message: <b>@parentMessage</b></p>

<p>
    <button @onclick="ChangeValue">Change from Parent</button>
</p>

<NestedChild @bind-ChildMessage="parentMessage" />

@code {
    private string parentMessage = "Initial value set in Parent";

    private void ChangeValue()
    {
        parentMessage = $"Set in Parent {DateTime.Now}";
    }
}
@page "/parent-2"

<h1>Parent Component</h1>

<p>Parent Message: <b>@parentMessage</b></p>

<p>
    <button @onclick="ChangeValue">Change from Parent</button>
</p>

<NestedChild @bind-ChildMessage="parentMessage" />

@code {
    private string parentMessage = "Initial value set in Parent";

    private void ChangeValue()
    {
        parentMessage = $"Set in Parent {DateTime.Now}";
    }
}
@page "/parent-2"

<h1>Parent Component</h1>

<p>Parent Message: <b>@parentMessage</b></p>

<p>
    <button @onclick="ChangeValue">Change from Parent</button>
</p>

<NestedChild @bind-ChildMessage="parentMessage" />

@code {
    private string parentMessage = "Initial value set in Parent";

    private void ChangeValue()
    {
        parentMessage = $"Set in Parent {DateTime.Now}";
    }
}

En el siguiente componente NestedChild , el componente NestedGrandchild:

  • Asigna el valor de ChildMessage a GrandchildMessage con sintaxis de @bind:get.
  • Actualiza GrandchildMessage cuando se ejecuta ChildMessageChanged con la sintaxis de @bind:set.

NestedChild.razor:

<div class="border rounded m-1 p-1">
    <h2>Child Component</h2>

    <p>Child Message: <b>@ChildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Child</button>
    </p>

    <NestedGrandchild @bind-GrandchildMessage:get="ChildMessage" 
        @bind-GrandchildMessage:set="ChildMessageChanged" />
</div>

@code {
    [Parameter]
    public string? ChildMessage { get; set; }

    [Parameter]
    public EventCallback<string?> ChildMessageChanged { get; set; }

    private async Task ChangeValue() => 
        await ChildMessageChanged.InvokeAsync($"Set in Child {DateTime.Now}");
}
<div class="border rounded m-1 p-1">
    <h2>Child Component</h2>

    <p>Child Message: <b>@ChildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Child</button>
    </p>

    <NestedGrandchild @bind-GrandchildMessage:get="ChildMessage" 
        @bind-GrandchildMessage:set="ChildMessageChanged" />
</div>

@code {
    [Parameter]
    public string? ChildMessage { get; set; }

    [Parameter]
    public EventCallback<string> ChildMessageChanged { get; set; }

    private async Task ChangeValue()
    {
        await ChildMessageChanged.InvokeAsync(
            $"Set in Child {DateTime.Now}");
    }
}
<div class="border rounded m-1 p-1">
    <h2>Child Component</h2>

    <p>Child Message: <b>@ChildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Child</button>
    </p>

    <NestedGrandchild @bind-GrandchildMessage="BoundValue" />
</div>

@code {
    [Parameter]
    public string? ChildMessage { get; set; }

    [Parameter]
    public EventCallback<string> ChildMessageChanged { get; set; }

    private string BoundValue
    {
        get => ChildMessage ?? string.Empty;
        set => ChildMessageChanged.InvokeAsync(value);
    }

    private async Task ChangeValue()
    {
        await ChildMessageChanged.InvokeAsync(
            $"Set in Child {DateTime.Now}");
    }
}
<div class="border rounded m-1 p-1">
    <h2>Child Component</h2>

    <p>Child Message: <b>@ChildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Child</button>
    </p>

    <NestedGrandchild @bind-GrandchildMessage="BoundValue" />
</div>

@code {
    [Parameter]
    public string ChildMessage { get; set; }

    [Parameter]
    public EventCallback<string> ChildMessageChanged { get; set; }

    private string BoundValue
    {
        get => ChildMessage;
        set => ChildMessageChanged.InvokeAsync(value);
    }

    private async Task ChangeValue()
    {
        await ChildMessageChanged.InvokeAsync(
            $"Set in Child {DateTime.Now}");
    }
}
<div class="border rounded m-1 p-1">
    <h2>Child Component</h2>

    <p>Child Message: <b>@ChildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Child</button>
    </p>

    <NestedGrandchild @bind-GrandchildMessage="BoundValue" />
</div>

@code {
    [Parameter]
    public string ChildMessage { get; set; }

    [Parameter]
    public EventCallback<string> ChildMessageChanged { get; set; }

    private string BoundValue
    {
        get => ChildMessage;
        set => ChildMessageChanged.InvokeAsync(value);
    }

    private async Task ChangeValue()
    {
        await ChildMessageChanged.InvokeAsync(
            $"Set in Child {DateTime.Now}");
    }
}

Advertencia

Por lo general, evita crear componentes que escriban directamente en sus propios parámetros de componente. El componente anterior NestedChild usa una propiedad BoundValue en lugar de escribir directamente en su parámetro ChildMessage. Para obtener más información, consulta Evitar sobrescribir parámetros en ASP.NET Core Blazor.

NestedGrandchild.razor:

<div class="border rounded m-1 p-1">
    <h3>Grandchild Component</h3>

    <p>Grandchild Message: <b>@GrandchildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Grandchild</button>
    </p>
</div>

@code {
    [Parameter]
    public string? GrandchildMessage { get; set; }

    [Parameter]
    public EventCallback<string> GrandchildMessageChanged { get; set; }

    private async Task ChangeValue() => 
        await GrandchildMessageChanged.InvokeAsync(
            $"Set in Grandchild {DateTime.Now}");
}
<div class="border rounded m-1 p-1">
    <h3>Grandchild Component</h3>

    <p>Grandchild Message: <b>@GrandchildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Grandchild</button>
    </p>
</div>

@code {
    [Parameter]
    public string? GrandchildMessage { get; set; }

    [Parameter]
    public EventCallback<string> GrandchildMessageChanged { get; set; }

    private async Task ChangeValue()
    {
        await GrandchildMessageChanged.InvokeAsync(
            $"Set in Grandchild {DateTime.Now}");
    }
}
<div class="border rounded m-1 p-1">
    <h3>Grandchild Component</h3>

    <p>Grandchild Message: <b>@GrandchildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Grandchild</button>
    </p>
</div>

@code {
    [Parameter]
    public string? GrandchildMessage { get; set; }

    [Parameter]
    public EventCallback<string> GrandchildMessageChanged { get; set; }

    private async Task ChangeValue()
    {
        await GrandchildMessageChanged.InvokeAsync(
            $"Set in Grandchild {DateTime.Now}");
    }
}
<div class="border rounded m-1 p-1">
    <h3>Grandchild Component</h3>

    <p>Grandchild Message: <b>@GrandchildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Grandchild</button>
    </p>
</div>

@code {
    [Parameter]
    public string GrandchildMessage { get; set; }

    [Parameter]
    public EventCallback<string> GrandchildMessageChanged { get; set; }

    private async Task ChangeValue()
    {
        await GrandchildMessageChanged.InvokeAsync(
            $"Set in Grandchild {DateTime.Now}");
    }
}
<div class="border rounded m-1 p-1">
    <h3>Grandchild Component</h3>

    <p>Grandchild Message: <b>@GrandchildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Grandchild</button>
    </p>
</div>

@code {
    [Parameter]
    public string GrandchildMessage { get; set; }

    [Parameter]
    public EventCallback<string> GrandchildMessageChanged { get; set; }

    private async Task ChangeValue()
    {
        await GrandchildMessageChanged.InvokeAsync(
            $"Set in Grandchild {DateTime.Now}");
    }
}

Para obtener un enfoque alternativo adecuado a fin de compartir datos en memoria y entre componentes que no están necesariamente anidados, consulta Administración de estado de Blazor en ASP.NET Core.

Campo enlazado o árbol de expresión de propiedad

Para facilitar interacciones más profundas con un enlace, Blazor te permite capturar el árbol de expresión de un campo o propiedad enlazados. Esto se logra definiendo una propiedad con el nombre de campo o propiedad sufijo con Expression. Para cualquier campo o propiedad determinada denominada {FIELD OR PROPERTY NAME}, la propiedad de árbol de expresión correspondiente se denomina {FIELD OR PROPERTY NAME}Expression.

El siguiente componente ChildParameterExpression identifica el nombre de campo y el modelo de la expresión Year. Un FieldIdentifier, que se usa para obtener el nombre del campo y el modelo, identifica de forma única un único campo que se puede editar. Puede corresponder a una propiedad en un objeto de modelo o puede ser cualquier otro valor con nombre. El uso de la expresión de un parámetro es útil al crear componentes de validación personalizados, que no están cubiertos por la documentación de Microsoft Blazor, pero se aborda mediante numerosos recursos de terceros.

ChildParameterExpression.razor:

@using System.Linq.Expressions

<ul>
    <li>Year model: @yearField.Model</li>
    <li>Year field name: @yearField.FieldName</li>
</ul>

@code {
    private FieldIdentifier yearField;

    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    [Parameter]
    public Expression<Func<int>> YearExpression { get; set; } = default!;

    protected override void OnInitialized() => 
        yearField = FieldIdentifier.Create(YearExpression);
}
@using System.Linq.Expressions

<ul>
    <li>Year model: @yearField.Model</li>
    <li>Year field name: @yearField.FieldName</li>
</ul>

@code {
    private FieldIdentifier yearField;

    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    [Parameter]
    public Expression<Func<int>> YearExpression { get; set; } = default!;

    protected override void OnInitialized()
    {
        yearField = FieldIdentifier.Create(YearExpression);
    }
}
@using System.Linq.Expressions

<ul>
    <li>Year model: @yearField.Model</li>
    <li>Year field name: @yearField.FieldName</li>
</ul>

@code {
    private FieldIdentifier yearField;

    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    [Parameter]
    public Expression<Func<int>> YearExpression { get; set; } = default!;

    protected override void OnInitialized()
    {
        yearField = FieldIdentifier.Create(YearExpression);
    }
}
@using System.Linq.Expressions

<ul>
    <li>Year model: @yearField.Model</li>
    <li>Year field name: @yearField.FieldName</li>
</ul>

@code {
    private FieldIdentifier yearField;

    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    [Parameter]
    public Expression<Func<int>> YearExpression { get; set; } = default!;

    protected override void OnInitialized()
    {
        yearField = FieldIdentifier.Create(YearExpression);
    }
}
@using System.Linq.Expressions

<ul>
    <li>Year model: @yearField.Model</li>
    <li>Year field name: @yearField.FieldName</li>
</ul>

@code {
    private FieldIdentifier yearField;

    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    [Parameter]
    public Expression<Func<int>> YearExpression { get; set; } = default!;

    protected override void OnInitialized()
    {
        yearField = FieldIdentifier.Create(YearExpression);
    }
}

Parent3.razor:

@page "/parent-3"

<PageTitle>Parent 3</PageTitle>

<h1>Parent Example 3</h1>

<p>Parent <code>year</code>: @year</p>

<ChildParameterExpression @bind-Year="year" />

@code {
    private int year = 1979;
}
@page "/parent-3"

<h1>Parent Example 3</h1>

<p>Parent <code>year</code>: @year</p>

<ChildParameterExpression @bind-Year="year" />

@code {
    private int year = 1979;
}
@page "/parent-3"

<h1>Parent Example 3</h1>

<p>Parent <code>year</code>: @year</p>

<ChildParameterExpression @bind-Year="year" />

@code {
    private int year = 1979;
}
@page "/parent-3"

<h1>Parent Example 3</h1>

<p>Parent <code>year</code>: @year</p>

<ChildParameterExpression @bind-Year="year" />

@code {
    private int year = 1979;
}
@page "/parent-3"

<h1>Parent Example 3</h1>

<p>Parent <code>year</code>: @year</p>

<ChildParameterExpression @bind-Year="year" />

@code {
    private int year = 1979;
}

Recursos adicionales