Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

18 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 19 de mayo de 2020
Blazor Normalmente, en demos y ejemplos de Blazor solemos ver el uso componentes sin cuerpo, muchas veces escritos en forma de tags autocerrados, como los siguientes:
<Article Title="A new hope" DatePublished="@DateTime.Now"></Article>
<Article Title="A new hope" DatePublished="@DateTime.Now" />
El código de este componente, en Article.razor, podría ser el siguiente:
<h1>@Title</h1>
<p>Post content goes here...</p>
@code {
    [Parameter]
    public string Title { get; set; }
    [Parameter]
    public DateTime DatePublished { get; set; }
}
En este punto, es lógico pensar que nuestro artículo tendrá su texto, por lo que deberíamos poder incluirle un cuerpo. Una posibilidad sería incluir en el componente una propiedad Body y enviarle el cuerpo en el momento de su instanciación, como en el ejemplo que vemos a continuación:
<Article Title="A new hope" DatePublished="@DateTime.Now" Body="This is the post content..."></Article>
Pero, como podréis imaginar, esto es bastante limitado. Aparte de la legibilidad nula que tendría a nivel de código si el texto es medianamente extenso, no podríamos utilizar marcado HTML ni otros componentes Blazor en su interior, por lo que no es una solución práctica. Lo que en realidad nos interesa es poder hacer lo siguiente:
<Article Title="A new hope" DatePublished="@DateTime.Now">
    <p>This is the post content</p>
    <p>Blah, blah, blah...</p>
</Article>
Sin embargo, si probáis a ejecutar el código anterior obtendréis un error parecido al siguiente:
InvalidOperationException: Object of type 'MyBlazorProject.Pages.Article' does not have a property matching the name 'ChildContent'.
El mensaje de error no nos indica por qué ocurre, aunque apunta en la dirección para poder solucionarlo. Como no podría ser de otra forma, Blazor permite la creación de componentes con cuerpo.

La propiedad ChildContent

Cuando Blazor parsea un componente con cuerpo, intenta introducirlo en una propiedad de tipo RenderFragment que, por convención, se denomina ChildContent. Como podréis adivinar, cuando no la encuentra se lanza la excepción que veíamos anteriormente.

Reescribamos nuestro componente para incluirle esta propiedad:
<h1>@Title</h1>
<div class="content">
    @ChildContent
</div>

@code {
    [Parameter]
    public string Title { get; set; }
    [Parameter]
    public DateTime DatePublished { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }
}
Fijaos especialmente en dos cosas:
  • Primero, atendiendo a la convención, hemos añadido la propiedad ChildContent de tipo RenderFragment. En esta propiedad se introducirá el contenido especificado en el cuerpo del componente en el momento de utilizarlo.
     
  • Segundo, algo más arriba, justo debajo del título del post, hemos insertado @ChildContent en la zona de marcado HTML donde queremos que sea renderizado el cuerpo del componente.
De esta forma, ya podemos suministrar a <Article> el cuerpo con toda la riqueza que Blazor nos permite, incluyendo tanto tags HTML como construcciones Razor:
<Article Title="A new hope" DatePublished="@DateTime.Now">
    <p>This is the post content</p>
    <p>Today is @DateTime.Now</p>
    <p>Blah, blah, blah...</p>
</Article>
Fácil, ¿verdad?

Aquí podríamos dar por terminado el post, pero todavía nos quedan cosas interesantes por descubrir...

¿Se pueden tener otras propiedades de tipo RenderFragment en el componente?

Pues sí, y de hecho puede resultar bastante útil. Imaginad que queremos añadir a nuestro componente <Article> la posibilidad de utilizar en su cuerpo tags como <Summary> y <Body> para especificar el resumen y el contenido del artículo de la siguiente forma:
<Article Title="A new hope" DatePublished="@DateTime.Now">
    <Summary>
        This article talks about a new hope.
    </Summary>
    <Body>
        <p>This is the post content</p>
        <p>Today is @DateTime.Now</p>
        <p>Blah, blah, blah...</p>
    </Body>
</Article>
Este sería un caso de uso de múltiples RenderFragment. Observad que aquí ya no nos interesa acceder al cuerpo completo del componente mediante la propiedad ChildContent, sino que queremos obtener por separado el sumario y el cuerpo.

Para ello, la implementación del componente podría ser la siguiente:
<h1>@Title</h1>
<div class="summary">
    @Summary
</div>
<div class="content">
    @Body
</div>

@code {
    [Parameter]
    public string Title { get; set; }
    [Parameter]
    public DateTime DatePublished { get; set; }

    [Parameter]
    public RenderFragment Summary { get; set; }
    [Parameter]
    public RenderFragment Body { get; set; }
}

¿Y podemos enviarle datos a un RenderFragment?

Pues sí, y conceptualmente es algo muy parecido a lo que hacemos cuando en MVC enviamos a una vista tipada los datos que necesita para maquetarse. En el caso Blazor, podemos definir un fragmento como RenderFragment<T>, siendo T el tipo de datos que le suministraremos en el momento de renderizarlo.

Así, cuando un RenderFragment<T> es renderizado, es necesario enviarle una instancia de T. Es decir, ya no incluiremos el fragmento en el marcado utilizando una expresión como @Fragmento (como hacíamos antes con @Summary o @Body), sino @Fragmento(obj), siendo obj la instancia de T que actuará como contexto de renderizado del fragmento.

Por ejemplo, continuando con el escenario anterior, podríamos definir nuestro componente <Article> como aparece algo más abajo, donde indicamos que tanto Summary como Body son fragmentos tipados que recibirán una instancia de Article, el componente que los contiene. O en otras palabras, Summary y Body son fragmentos que se renderizarán usando como contexto un objeto de tipo Article:
[Parameter]
public RenderFragment<Article> Body { get; set; }
[Parameter]
public RenderFragment<Article> Summary { get; set; }
Ya en el marcado HTML, en el momento de renderizar estos fragmentos debemos suministrarles la instancia que actuará como contexto. Dado que en este caso estamos en el interior de la clase Article, podemos utilizar this:
<h1>@Title</h1>
<div class="summary">
    @Summary(this)
</div>
<div class="content">
    @Body(this)
</div>

@code {
    [Parameter]
    public string Title { get; set; }
    [Parameter]
    public DateTime DatePublished { get; set; }

    [Parameter]
    public RenderFragment<Article> Body { get; set; }
    [Parameter]
    public RenderFragment<Article> Summary { get; set; }
}
¿Y cómo podemos utilizar el contexto a la hora de definir el contenido de los fragmentos? Pues sencillo. Por defecto, en el cuerpo de estos fragmentos tendremos definida una variable llamada context que nos dará acceso al contexto:
<Article Title="A new hope" DatePublished="@DateTime.Now">
    <Summary>
        This article was posted on @context.DatePublished.ToShortDateString() 
        and talks about @context.Title.
    </Summary>
    <Body>
        <p>This is the post content</p>
        <p>Today is @DateTime.Now</p>
        <p>Blah, blah, blah...</p>
    </Body>
</Article>
Si el nombre context no nos convence demasiado podemos utilizar la propiedad Context de los RenderFragment para indicar el nombre de la variable con la que podremos acceder desde ellos a sus datos de contexto. Es decir, con  Context indicaremos el nombre que daremos localmente a dicha información para poder referirnos a ella.

Quizás suene un poco confuso, pero una vez lo pillamos es simple. En el siguiente bloque de código utilizamos un componente <Article> en cuyo fragmento <Summary> especificamos que el contexto estará disponible localmente bajo el nombre article (que, como sabemos, es de tipo Article). Por esa razón, podemos acceder a él mediante @article:
<Article Title="A new hope" DatePublished="@DateTime.Now">
    <Summary Context="article">
        This article was posted on @article.DatePublished.ToShortDateString() 
        and talks about @article.Title.
    </Summary>
    <Body>
        <p>This is the post content</p>
        <p>Today is @DateTime.Now</p>
        <p>Blah, blah, blah...</p>
    </Body>
</Article>
Si lo pensáis un poco, veréis que este patrón es el mismo que se utiliza en el componente de routing de la aplicación que encontraréis en el archivo App.razor de todos los proyectos Blazor. En él, el fragmento <Found>, que es un RenderFragment<RouteData>, establece que su contexto estará disponible en la variable routeData, que es luego utilizada para proporcionar a <RouteView> el valor del atributo RouteData:
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    ...
</Router>
Espero que os resulte útil en vuestros desarrollos :)

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!