En Blazor, ¿cómo puedo deshacer una entrada de usuario no válida sin cambiar el estado del componente para activar una nueva representación?
Aquí hay un ejemplo simple de contador de Blazor ( pruébelo en línea ):
<label>Count:</label> <button @onclick=Increment>@count times!</button><br> A: <input @oninput=OnChange value="@count"><br> B: <input @bind-value=count @bind-value:event="oninput"> @code { int count = 1; void Increment() => count++; void OnChange(ChangeEventArgs e) { var userValue = e.Value?.ToString(); if (int.TryParse(userValue, out var v)) { count = v; } else { if (String.IsNullOrWhiteSpace(userValue)) { count = 0; } // if count hasn't changed here, // I want to re-render "A" // this doesn't work e.Value = count.ToString(); // this doesn't work either StateHasChanged(); } } }
Para el elemento de input
A, quiero replicar el comportamiento del elemento de input
B, pero sin usar los atributos de enlace de datos de estilo bind-xxx
.
Por ejemplo, cuando 123x
dentro de A, quiero que vuelva a 123
automáticamente, como sucede con B.
StateHasChanged
pero no funciona, supongo, porque la propiedad de count
en realidad no cambia.
Entonces, básicamente necesito volver a renderizar A para deshacer la entrada de usuario no válida, aunque el estado no haya cambiado. ¿Cómo puedo hacer eso sin la magia bind-xxx
?
Claro, bind-xxx
es excelente, pero hay casos en los que se puede desear un comportamiento no estándar, construido alrededor de un controlador de eventos administrado como ChangeEvent
.
Actualizado , para comparar, así es como podría haberlo hecho en React ( pruébelo en línea ):
function App() { let [count, setCount] = useState(1); const handleClick = () => setCount((count) => count + 1); const handleChange = (e) => { const userValue = e.target.value; let newValue = userValue ? parseInt(userValue) : 0; if (isNaN(newValue)) newValue = count; // re-render even when count hasn't changed setCount(newValue); }; return ( <> Count: <button onClick={handleClick}>{count}</button><br/> A: <input value={count} onInput={handleChange}/><br/> </> ); }
Además, así es como podría haberlo hecho en Svelte, que me parece conceptualmente muy parecido a Blazor ( pruébelo en línea ).
<script> let count = 1; const handleClick = () => count++; const handleChange = e => { const userValue = e.target.value; let newValue = userValue? parseInt(userValue): 0; if (isNaN(newValue)) newValue = count; if (newValue === count) e.target.value = count; // undo user input else count = newValue; } }; </script> Count: <button on:click={handleClick}>{count}</button><br/> A: <input value={count} on:input={handleChange}/><br/>
Actualizado , para aclarar, simplemente quiero deshacer lo que considere una entrada no válida, retrospectivamente después de que haya sucedido, al manejar el evento de cambio, sin mutar el estado del componente en sí ( counter
aquí).
Es decir, sin el enlace de datos bidireccional específico de Blazor, el type=number
o los atributos de coincidencia de patrones. Simplemente uso el requisito de formato de número aquí como ejemplo; Quiero poder deshacer cualquier entrada arbitraria como esa.
La experiencia de usuario que quiero (hecha a través de un truco de interoperabilidad JS): https://blazorrepl.telerik.com/wPbvcvvi128Qtzvu03
Me sorprendió que esto fuera tan difícil en Blazor en comparación con otros marcos, y que no puedo usar StateHasChanged
para simplemente forzar una nueva representación del componente en su estado actual.
Aquí hay una versión modificada de su código que hace lo que usted quiere:
@page "/" <label>Count:</label> <button @onclick=Increment>@count times!</button> <br> A: <input @oninput=OnChange value="@count"> <br> B: <input @bind-value=count @bind-value:event="oninput"> @code { int count = 1; void Increment() => count++; async Task OnChange(ChangeEventArgs e) { var oldvalue = count; var isNewValue = int.TryParse(e.Value?.ToString(), out var v); if (isNewValue) count = v; else { count = 0; // this one line may precipitate a few commments! await Task.Yield(); count = oldvalue; } } }
Así que "¿Qué está pasando?"
En primer lugar, el código de afeitar está precompilado en clases de C#, por lo que lo que ve no es lo que realmente se ejecuta como código. No entraré en eso aquí, hay muchos artículos en línea.
value="@count"
es un enlace unidireccional y se pasa como una cadena.
Puede cambiar el valor real en la entrada en pantalla, pero en el componente Blazor el valor sigue siendo el valor anterior. No ha habido devolución de llamada para decirle lo contrario.
Cuando escribe 22x después de 22, OnChange
no actualiza count
. En lo que respecta al Renderer, no ha cambiado, por lo que no es necesario actualizar esa parte del DOM. ¡Tenemos una discrepancia entre el Renderer DOM y el DOM real!
OnChange
cambia a async Task
y ahora:
count
.count
en otro valor, en este caso cero.StateHasChanged
y produce. Esto le da tiempo al subproceso Renderer para atender su cola y volver a renderizar. La entrada en momentáneamente cero.count
en el valor anterior.StateHasChanged
por segunda vez. El renderizador actualiza el valor de visualización.El controlador de eventos del componente Blazor básico [BCEH from this point] se ve así:
var task = InvokeAsync(EventMethod); StateHasChanged(); if (!task.IsCompleted) { await task; StateHasChanged(); }
Pon OnChange
en este contexto.
var task = InvokeAsync(EventMethod)
ejecuta OnChange
. Que comienza a funcionar sincrónicamente.
Si isNewValue
es falso, se establece el count
en 0 y luego se produce a través Task.Yield
pasando una tarea incompleta de vuelta a BCEH. Esto puede progresar y ejecutar StateHasChanged
, que pone en cola un fragmento de renderizado en la cola del Renderer. Tenga en cuenta que en realidad no procesa el componente, solo pone en cola el fragmento de procesamiento. En este punto, BCEH está acaparando el hilo, por lo que Renderer no puede atender su cola. Luego verifica la task
para ver si se completó.
Si está completo, BCEH se completa, Renderer obtiene algo de tiempo de subproceso y procesa el componente.
Si aún se está ejecutando, será como si lo hubiésemos puesto al final de la cola de subprocesos con Task.Yield: BCEH lo espera y cede. Renderer obtiene algo de tiempo de subproceso y renderiza el componente. OnChange
luego se completa, BCEH obtiene una tarea completa, apila otro procesamiento en la cola de procesamiento con una llamada a StateHasChanged
y se completa. El renderizador, ahora con servicios de tiempo de subprocesos, está en cola y renderiza el componente por segunda vez.
Tenga en cuenta que algunas personas prefieren usar Task.Delay(1)
, porque hay cierta discusión sobre cómo funciona exactamente Task.Yield
.
Esta fue una pregunta muy interesante. Nunca antes había usado Blazor, pero tenía una idea sobre lo que podría ayudar aquí, aunque esta también es una respuesta engañosa.
Noté que si cambiaba el elemento para vincularlo a la variable de conteo, actualizaría el valor cuando el control perdiera el foco. Así que agregué algo de código para intercambiar elementos de enfoque. Esto parece permitir escribir caracteres no numéricos sin cambiar el campo de entrada, que es lo que creo que se desea aquí.
Obviamente, no es una solución fantástica, pero pensé en ofrecerla en caso de que ayude.
<label>Count:</label> <button @onclick=Increment>@count times!</button><br> A: <input @oninput=OnChange @bind-value=count @ref="textInput1"><br> B: <input @bind-value=count @bind-value:event="oninput" @ref="textInput2"> @code { ElementReference textInput1; ElementReference textInput2; int count = 1; void Increment() => count++; void OnChange(ChangeEventArgs e) { var userValue = e.Value?.ToString(); if (int.TryParse(userValue, out var v)) { count = v; } else { if (String.IsNullOrWhiteSpace(userValue)) count = 0; e.Value = count.ToString(); textInput2.FocusAsync(); textInput1.FocusAsync(); } } }
Entonces, básicamente necesito volver a renderizar A para deshacer la entrada de usuario no válida, aunque el estado no haya cambiado. ¿Cómo puedo hacer eso sin la magia bind-xxx?
Puede forzar la recreación y la reproducción de cualquier elemento/componente cambiando el valor en la directiva @key
:
<input @oninput=OnChange value="@count" @key="version" />
void OnChange(ChangeEventArgs e) { var userValue = e.Value?.ToString(); if (int.TryParse(userValue, out var v)) { count = v; } else { version++; if (String.IsNullOrWhiteSpace(userValue)) { count = 0; } } }
Tenga en cuenta que volverá a representar todo el elemento (y su subárbol), no solo el atributo.
El problema con su código es que el método BuildRendrerTree de su componente genera exactamente el mismo RenderTree, por lo que el algoritmo de diferenciación no encuentra nada para actualizar en el dominio real.
Entonces, ¿por qué funciona la directiva @bind?
Observe el código BuildRenderTree generado:
//A __builder.OpenElement(6, "input"); __builder.AddAttribute(7, "oninput", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.ChangeEventArgs>(this, OnChange)); __builder.AddAttribute(8, "value", count); __builder.CloseElement(); //B __builder.AddMarkupContent(9, "<br>\r\nB: "); __builder.OpenElement(10, "input"); __builder.AddAttribute(11, "value", Microsoft.AspNetCore.Components.BindConverter.FormatValue(count)); __builder.AddAttribute(12, "oninput", Microsoft.AspNetCore.Components.EventCallback.Factory.CreateBinder(this, __value => count = __value, count)); __builder.SetUpdatesAttributeName("value"); __builder.CloseElement();
El truco es que la directiva @bind agrega:
__builder.SetUpdatesAttributeName("value");
No puede hacer esto en el marcado para EventCallback en este momento, pero hay un problema abierto para ello: https://github.com/dotnet/aspnetcore/issues/17281
Sin embargo, aún puede crear un Componente o RenderFragment y escribir el código __builder
manualmente.
A continuación se muestra lo que se me ocurrió ( pruebe en línea ). Desearía que ChangeEventArgs.Value
funcionara en ambos sentidos, pero no es así. Sin él, solo puedo pensar en este truco de JS.InvokeVoidAsync
:
@inject IJSRuntime JS <label>Count:</label> <button @onclick=Increment>@count times!</button> <br> A: <input @oninput=OnChange value="@count" id="@id"> <br> B: <input @bind-value=count @bind-value:event="oninput"> @code { int count = 1; string id = Guid.NewGuid().ToString("N"); void Increment() => count++; async Task OnChange(ChangeEventArgs e) { var userValue = e.Value?.ToString(); if (int.TryParse(userValue, out var v)) { count = v; } else { if (String.IsNullOrWhiteSpace(userValue)) { count = 0; } // this doesn't work e.Value = count.ToString(); // this doesn't work either (no rererendering): StateHasChanged(); // using JS interop as a workaround await JS.InvokeVoidAsync("eval", $"document.getElementById('{id}').value = Number('{count}')"); } } }
Para ser claros, me doy cuenta de que este es un truco horrible (por último, pero no menos importante, porque usa eval
); Posiblemente podría mejorarlo usando ElementReference
y una importación JS aislada, pero todo eso no sería necesario si e.Value = count
funcionara, como lo hace con Svelte . He planteado esto como un problema de Blazor en el repositorio de ASP.NET , espero que pueda llamar la atención.
Al mirar el código, ¿parece que quieres desinfectar la entrada del usuario? Por ejemplo, puede ingresar solo números, formatos de fecha, etc. Estoy de acuerdo en que es un poco difícil si desea usar los controladores de eventos manualmente en este sentido en este momento, pero aún es posible validar la entrada usando propiedades expandidas y el sistema de enlace de Blazor.
Lo que quieres se vería así: ( pruébalo aquí )
<label>Count: @Count</label> <button @onclick=Increment>@Count times!</button><br> <input @bind-value=Count @bind-value:event="oninput"> @code { int _count = 1; public string Count { get => _count.ToString(); set { if(int.TryParse(value, out var val)) { _count = val; } else { if(string.IsNullOrEmpty(value)) { _count = 0; } } } } void Increment() => _count++; }
Puede usar @on{DOM EVENT}:preventDefault
para evitar la acción predeterminada para un evento. Para obtener más información, consulte los documentos de Microsoft: https://docs.microsoft.com/en-us/aspnet/core/blazor/components/event-handling?view=aspnetcore-6.0#prevent-default-actions
ACTUALIZAR
Un ejemplo del uso de preventDefault
<label>Count:</label> <button @onclick=Increment>@count times!</button><br /> <br> A: <input value="@count" @onkeydown=KeyHandler @onkeydown:preventDefault><br> B: <input @bind-value=count @bind-value:event="oninput"> @code { int count = 1; string input = ""; void Increment() => count++; private void KeyHandler(KeyboardEventArgs e) { //Define your pattern var pattern = "abcdefghijklmnopqrstuvwxyz"; if (!pattern.Contains(e.Key) && e.Key != "Backspace") { //This is just a sample and needs exception handling input = input + e.Key; count = int.Parse(input); } if (e.Key == "Backspace") { input = input.Remove(input.Length - 1); count = int.Parse(input); } } }
Deberá usar @on{DOM EVENT}-preventDefault
.
Viene del mundo de JavaScript y evita el comportamiento predeterminado del botón.
En blazor y razor, la validación o DDL activa la devolución de datos, lo que significa que el servidor procesa la solicitud y se vuelve a procesar.
Al hacer esto, debe asegurarse de que su evento no "burbujee", ya que está impidiendo que el evento realice una devolución de datos.
Si encuentra que su evento burbujea, lo que significa que el evento del elemento sube a los elementos en los que está anidado, use:
stopPropagation="true";
para más información: