Propiedades, comandos y eventos

18 de Julio de 2005

Publicado: 8 de diciembre de 2004

Chris Anderson

Microsoft Corporation

Resumen: Chris Anderson muestra el modo de generar un control de expandir o contraer mediante el uso de las propiedades, controles y eventos disponibles en la última versión de "Avalon". (18 páginas impresas.)

Descargue el archivo ControlBlocks%20source.msi.

En esta página

Grandes cambios Grandes cambios
Propiedades Propiedades
Comandos Comandos
Eventos Eventos
Una vez más, con pasión Una vez más, con pasión
Resumen Resumen

Grandes cambios

Mucho tiempo ha transcurrido desde nuestra última conversación y desde entonces se han hecho públicos importantes anuncios con respecto a "Longhorn," WinFX y "Avalon". Estos cambios me parecen de enorme interés, sobre todo porque la posibilidad de ejecutar Avalon en más versiones de Windows permite a los desarrolladores aprovechar antes esta nueva funcionalidad. Este artículo se centra en la versión Community Technical Preview de Avalon, de reciente lanzamiento, y esto implica que utilizaremos las últimas y más importantes novedades.

Jeff Bogdan abordó el modelo de contenido de control en su último artículo, The Avalon Control Content Model (en inglés). Hoy explicaremos otros conceptos clave del modelo de contenido de Avalon. Aunque la mayor parte de los desarrolladores ya están probablemente familiarizados con las propiedades y los eventos, existen nuevos y eficaces servicios en Avalon que es interesante conocer. Los comandos, que pueden resultar un concepto nuevo para muchos lectores, permiten a un control publicar sus acciones que, a continuación, otros componentes podrán enlazar. Mediante la combinación del modelo de contenido con propiedades, eventos y comandos, se pueden crear controles de enorme flexibilidad y que requieren poco o nada de código para su generación o personalización.

Como en todos los artículos, necesitamos un problema que solucionar para mostrar nuestra actuación. Probaremos a generar un control de expandir o contraer, similar al que se ve en el panel izquierdo de una ventana del Explorador de Windows XP, como se muestra en la figura 1.

Figura 1. Control de expandir o contraer en el Explorador de Windows XP

Existen en este caso tres expansores:

  • Tareas de archivo y carpeta (expandido)

  • Otros sitios (contraído)

  • Detalles (expandido)

En lugar de crear un control que únicamente se parezca a este expansor con tema de Windows XP, vamos a definir un control flexible que pueda participar en el mecanismo de estilos de Avalon y permita a los desarrolladores de aplicaciones personalizar el aspecto y el comportamiento del control.

En su último artículo, Jeff explicaba que se pueden utilizar algunas de las clases base de Avalon para proporcionar automáticamente nuestro modelo de contenido. En este caso, parece que cada expansor incluye un encabezado (círculo rojo) y un contenido (círculo verde), como se muestra en la figura 2.

Figura 2. Encabezado y desglose del contenido de los expansores

En Avalon, existe un elemento HeaderedContentControl que proporciona el modelo de contenido adecuado para este proceso. Al final, deberemos agregar datos, métodos y eventos adicionales, pero de momento definiremos el control del siguiente modo:

namespace InsideAvalon
{
    using System;
    using System.Windows;
    using System.Windows.Controls;
    public class Expander : HeaderedContentControl
    {
    }
}

Asimismo, necesitamos crear una aplicación de prueba básica para el control, así que utilizaremos XAML:

<Window
  xmlns="http://schemas.microsoft.com/2003/xaml"
  xmlns:x="Definition"
  xmlns:l="local"
  x:Class="InsideAvalon.ExpanderTest"
  Background="Beige"
  Text="Inside Avalon"
  >
</Window>

Incluiremos un archivo de código en segundo plano para la ventana (Window) y definiremos una aplicación del siguiente modo:

namespace InsideAvalon
{
    using System;
    using System.Windows;
    using System.Windows.Controls;
    public partial class ExpanderTest : Window
    {
    }
    #region Application
    class App : Application
    {
        [STAThread]
        static void Main()
        {
            new App().Run();
        }
        protected override void
            OnStartingUp(StartingUpCancelEventArgs e)
        {
            new ExpanderTest().Show();
        }
    }
    #endregion
}

El último paso consistirá en un archivo de proyecto MSBuild sencillo para compilar todo lo anterior:

<Project
  DefaultTargets="Build"
  xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <Configuration>Debug</Configuration>
    <TargetType>winexe</TargetType>
    <AssemblyName>expander</AssemblyName>
  </PropertyGroup>
  <Import Project="$(MSBuildBinPath)\Microsoft.CSHARP.targets" />
  <Import Project="$(MSBuildBinPath)\Microsoft.WinFX.targets" />
  <ItemGroup>
    <Compile Include="*.cs" />
    <Page Include="*.xaml" />
  </ItemGroup>
</Project>

Todos estos archivos se pueden encontrar en el código fuente de este artículo, en un directorio con el nombre Step1. Ahora que hemos resuelto las cuestiones preliminares, podemos encargarnos de los aspectos más interesantes: la creación del núcleo del control.

Propiedades

En la primera parte de la creación de un control debemos pensar en los datos asociados a dicho control. En el caso del expansor, necesitamos una propiedad booleana para controlar si el panel está expandido. La clase base se encarga de administrar el estado adicional del encabezado y del contenido.

Puede que algunos deseen comenzar a generar el control mediante la declaración de una sencilla propiedad CLR. Al fin y al cabo, éste es su funcionamiento actual:

public class Expander : HeaderedContentControl
{
    bool _isExpanded;
    public bool IsExpanded
    {
        get { return _isExpanded; }
        set { _isExpanded = value; }
    }
}

Este código se compilará correctamente y podrá tener acceso a la propiedad IsExpanded desde el marcado. Pensemos en todo el código que se necesitaría escribir para permitir estos servicios: activar las notificaciones de cambios, consultar el estado del motor de animación, buscar recursos y valores con estilo. Por ejemplo, a continuación se muestra un código hipotético (los estilos no funcionan de este modo) que se podría necesitar para obtener el valor de un estilo:

public class Expander : HeaderedContentControl
{
    bool _isExpanded;
    bool _isExpandedSet;
    public bool IsExpanded
    {
        get
        {
            if (!_isExpandedSet)
            {
                Style style = LoadStyle();
                if (style.IsPropertySet("IsExpanded"))
                {
                    return (bool)style.GetProperty("IsExpanded")
                }
            }
            return _isExpanded;
        }
        set
        {
            _isExpandedSet = true;
            if (_isExpanded != value)
            {
                _isExpanded = value;
                OnPropertyChanged("IsExpanded");
            }
        }
    }
}

Ahora le agregaremos compatibilidad para las animaciones:

public class Expander : HeaderedContentControl
{
    IList<BooleanAnimations> _isExpandedAnimations;
    bool _isExpanded;
    bool _isExpandedSet;
    public bool IsExpanded
    {
        get
        {
          if (_isExpandedAnimations != null&
                   _isExpandedAnimations.Count > 0)
            {
              // lots of code for calculating a current value!
              // ... go find the active timeline
              // ... calculate the value at the current time
              // ... oh, and do this even in the case the value
              //     comes from the style!
            }
            if (!_isExpandedSet)
            {
                Style style = LoadStyle();
                if (style.IsPropertySet("IsExpanded"))
                {
                    return (bool)style.GetProperty("IsExpanded")
                }
            }
            return _isExpanded;
        }
        set
        {
            _isExpandedSet = true;
            if (_isExpanded != value)
            {
                _isExpanded = value;
                OnPropertyChanged("IsExpanded");
            }
        }
    }
}

Si bien resulta extraño animar una propiedad booleana, existen otras muchas propiedades (como ancho, alto y colores, entre otras) que deseamos que permitan la animación. Este código continuará creciendo a medida que se agreguen servicios como el enlace de datos y se deberá clonar para cada propiedad que se defina. Además, se puede observar el rápido aumento de la cantidad de almacenamiento en el objeto de una única propiedad booleana.

Para no tener que escribir todo este código para cada propiedad que se defina, al tiempo que se ahorra memoria, existe un conjunto de código común denominado sistema de propiedades de dependencia, que permite definir con facilidad propiedades que admitan este código completo.

Este sistema se denomina de propiedades de dependencia porque uno de los principales servicios que ofrece es el control de dependencias entre los datos y la propiedad. Por ejemplo, el sistema de propiedades reconoce la dependencia existente entre el estilo y todos los objetos a los que éste afecta. De este modo, el sistema de propiedades puede invalidar todos los objetos necesarios cuando cambie el estilo (o la propiedad que afecta al mismo).

Para ahorrar espacio, el sistema de propiedades puede almacenar todos los datos adicionales de una propiedad en un almacén disperso asociado a cualquier objeto que se derive de DependencyObject. Al disponer de este almacén disperso, no existe una sobrecarga adicional para declarar una propiedad de un objeto, a menos que dicha propiedad se establezca en un valor que no sea el predeterminado. Hay más de 40 propiedades que afectan al procesamiento del texto en Avalon, por lo que si no contáramos con este almacenamiento disperso, la sobrecarga por elemento sería excesiva. La mayor parte de los elementos obtienen el conjunto de las propiedades de texto bien como predeterminadas, heredadas o desde un estilo y en ninguno de estos casos se agrega un coste adicional por instancia:

public class Expander : HeaderedContentControl
{
    public static DependencyProperty IsExpandedProperty;
    static Expander()
    {
        IsExpandedProperty = DependencyProperty.Register(
            "IsExpanded",
            typeof(bool),
            typeof(Expander));
    }
    public bool IsExpanded
    {
        get { return (bool)GetValueBase(IsExpandedProperty); }
        set { SetValueBase(IsExpandedProperty, value); }
    }
}

Mediante la declaración de DependecyProperty y el uso de GetValueBase y SetValueBase como implementación de la propiedad, el sistema de propiedades de dependencia puede ofrecer todos los servicios comunes anteriormente mencionados. De forma predeterminada, esta nueva propiedad puede tener estilo, enlace a datos y proporcionar notificaciones de cambios (como veremos más adelante en este artículo) con una mínima codificación por nuestra parte.

Para probar algunos de estos servicios, podemos utilizar el nuevo control de nuestra página de prueba. El primer paso consiste en importar el espacio de nombres CLR a XAML. Para ello, agregamos un PI de asignación en la parte superior del XAML y una definición de xmlns en la etiqueta raíz:

<?Mapping XmlNamespace="local" ClrNamespace="InsideAvalon" ?>
<Window
  xmlns="http://schemas.microsoft.com/2003/xaml"
  xmlns:x="Definition"
  xmlns:l="local"
  x:Class="InsideAvalon.ExpanderTest"
  Background="Beige"
  Text="Inside Avalon"
  >
...

He utilizado el espacio de nombres local para hacer referencia al espacio de nombres debido a que estos tipos se están definiendo en el proyecto de compilación. Por este mismo motivo, tampoco he incluido el atributo AssemblyName en el PI de asignación. El proceso de compilación de marcado evaluará de forma laxa las etiquetas para que el XAML de un proyecto pueda hacer referencia a elementos que aún no se han compilado.

Para probar nuestro control, debemos agregar un marcado que lo utilice:

<?Mapping XmlNamespace="local" ClrNamespace="InsideAvalon" ?>
<Window ... >
  <DockPanel>
    <l:Expander
      DockPanel.Dock="Top"
      Header="Red Expander"
      IsExpanded="true">
      <Rectangle Height="75" Fill="Red" />
    </l:Expander>
    <l:Expander
      DockPanel.Dock="Top"
      Header="Blue Expander"
      IsExpanded="false">
      <Rectangle Height="75" Fill="Blue" />
    </l:Expander>
  </DockPanel>
</Window>

Si se ejecuta la aplicación en este momento, no se apreciará nada extraordinario; se mostrará una ventana en blanco. ¿Por qué? Porque no hemos definido un aspecto para nuestro control. En este punto, existen al menos dos opciones para definir la apariencia del control. Se podría omitir el método OnRender en el control y utilizar comandos de dibujo para dibujar el aspecto. Alternativamente, se puede utilizar el sistema de estilos de Avalon y permitir a los desarrolladores de aplicaciones que definan el aspecto del control mediante marcado. Como deseamos crear un control muy flexible y permitir a los desarrolladores que creen aplicaciones interesantes, utilizaremos la segunda opción.

Si se está creando un control reutilizable, se deberá definir un estilo predeterminado y compilarlo en el proyecto. No obstante, para conseguir una mayor simplicidad, definiremos un estilo local:

<?Mapping XmlNamespace="local" ClrNamespace="InsideAvalon" ?>
<Window ...>
  <Window.Resources>
     <Style>
      <l:Expander />
      <Style.VisualTree>
        <Grid>
          <RowDefinition Height="Auto" />
          <RowDefinition Height="*" />
          <ContentPresenter
            Content="*Alias(Target=Header)"
            ContentStyle="*Alias(Target=HeaderStyle)"
       ContentStyleSelector="*Alias(Target=HeaderStyleSelector)"
            />
          <ContentPresenter
             x:StyleID="expanderContent"
             Grid.Row="1" />
        </Grid>
      </Style.VisualTree>
      <Style.VisualTriggers>
        <PropertyTrigger Property="IsExpanded" Value="False">
          <Set
            Target="expanderContent"
            PropertyPath="Visibility"
            Value="Collapsed" />
        </PropertyTrigger>
      </Style.VisualTriggers>
    </Style>
  </Window.Resources>
  ...
</Window>

Esto puede parecer complicado, pero cuando se analiza resulta increíblemente sencillo. Un estilo es un método genérico de aplicar propiedades a un conjunto de elementos. La primera etiqueta debajo del estilo define el objetivo de dicho estilo. En este caso, el control del expansor que acabamos de definir. Si deseáramos aplicar propiedades adicionales a todos los expansores (como fondo), se podrían incluir en esta etiqueta.

En la sección de VisualTree se ofrece una definición del aspecto visual de cualquier control (Jeff abordó esta cuestión en su último artículo). En el ejemplo anterior, he definido un control Grid de forma que contenga dos elementos que son ContentPresenters. El primer presentador de contenido se enlaza con la familia de propiedades de encabezado y el segundo enlaza con la familia de contenido de las propiedades (éste es el comportamiento predeterminado de un presentador de contenido).

La sección VisualTriggers consiste en un listado de acciones que se desean llevar a cabo en función de algunos criterios. En este caso, utilizamos el elemento PropertyTrigger para especificar que cuando la propiedad IsExpanded se establece en falso, deseamos que el segundo presentador de contenido esté contraído. La propiedad StyleID es un método para hacer referencia a un elemento con nombre de un VisualTree.

Cuando se ejecuta, se obtiene la visualización que se muestra en la figura 3.

Figura 3. Visualización del paso 2 con las etiquetas que aparecen en el código de ejemplo

La propiedad IsExpanded del expansor rojo estaba establecida como verdadero (true), por lo que se muestra un rectángulo. El expansor azul no estaba expandido, de modo que el rectángulo correspondiente está contraído. Ésta es la funcionalidad básica de un control de expansor. El contenido se puede mostrar u ocultar en función del estado de la propiedad IsExpanded. El siguiente paso consistirá en permitir al usuario expandir y contraer el control.

Comandos

En este punto, el control resulta bastante básico. Aunque se puede expandir y contraer el control mediante programación, no disponemos de un método predeterminado de controlar esta acción.

Nota. si se desea expandir y contraer el control en este punto, se puede agregar un botón que active y desactive la propiedad. Esto se denomina paso 2.5 en el código de ejemplo.

Para simplificar la programación, se puede agregar un método que active y desactive la expansión del control:

public class Expander : HeaderedContentControl
{
    ...
    public void ToggleExpanded()
    {
        IsExpanded = !IsExpanded;
    }
}

A continuación, deseamos agregar un botón al estilo predeterminado del expansor que llamará al método ToggleExpanded cuando el usuario haga clic en él. Anteriormente, vimos que se pueden utilizar desencadenadores de propiedades (una forma de enlace de datos) para conectar las propiedades y acciones en un estilo. Ahora necesitamos conectar la acción de un control en un estilo (haciendo clic en el botón) a una acción en el control con estilo (llamando al método ToggleExpanded).

Una opción consistiría en agregar código al botón para llamar al método ToggleExpanded en el controlador de eventos Click. Ésta no es una opción declarativa, lo que implica que para describir el aspecto del expansor debemos escribir código. Asimismo, deberíamos crear una nueva clase Button que implementara la lógica personalizada para que el controlador de sucesos Click llame al método ToggleExpanded específico.

En lugar de eso, utilizaremos un comando. Los comandos proporcionan una abstracción para llevar a cabo una acción, que habilita este enlace declarativo de la acción de un control a otro. Los comandos ofrecen un modo de enlazar a un método y obtener metadatos adicionales acerca de dicho método (por ejemplo, se puede determinar si el comando se debería habilitar).

Para comenzar, debemos definir el comando:

public class Expander : HeaderedContentControl
{
    public static Command ToggleExpandedCommand;
    public static DependencyProperty IsExpandedProperty;

    static Expander()
    {
        ToggleExpandedCommand = new Command(
            "ToggleExpanded",
            typeof(Expander),
            null);
        IsExpandedProperty = DependencyProperty.Register(
            "IsExpanded",
            typeof(bool),
            typeof(Expander));
    }
    ...
}

Una vez definido el comando, necesitamos que el expansor realice alguna acción en respuesta a dicho comando. Recordemos que el comando es una abstracción sobre la acción que se desea llevar a cabo. Proporciona un mecanismo para que otro control alcance el punto sobre el que queremos actuar y se denomina ToggleExpandedCommand. Ahora debemos decidir cómo queremos que el expansor interprete el comando.

using System.Windows.Commands;
public class Expander : HeaderedContentControl
{
    ...
    public Expander()
    {
        CommandBinding cb =
          new CommandBinding(ToggleExpandedCommand);
        cb.Execute +=
          new ExecuteEventHandler(ToggleExpandedExecute);
        CommandBindings.Add(cb);
    }
    void ToggleExpandedExecute(object sender, ExecuteEventArgs e)
    {
        ToggleExpanded();
    }
    ...
}

Para probarlo, podemos modificar el VisualTree del estilo, de modo que incluya un botón que enlace al comando que hemos creado.

<?Mapping XmlNamespace="local" ClrNamespace="InsideAvalon" ?>
<Window ...>
  <Window.Resources>
     <Style>
      <l:Expander />
      <Style.VisualTree>
        <Grid>
          <RowDefinition Height="Auto" />
          <RowDefinition Height="*" />

          <Grid>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />

            <ContentPresenter
              Content="*Alias(Target=Header)"
              ContentStyle="*Alias(Target=HeaderStyle)"
        ContentStyleSelector="*Alias(Target=HeaderStyleSelector)"
              />
            <Button
              Grid.Column="1"
              Command="l:Expander.ToggleExpandedCommand"
              Content="*" />
          </Grid>
  ...
</Window>

Al ejecutar el código ahora se obtiene el control del expansor totalmente funcional aunque limitado que se muestra en la figura 4.

Figura 4. Control de expansor totalmente funcional (paso 3 en el código de ejemplo)

Del mismo modo que proporcionar un modelo de datos sencillo mediante propiedades dinámicas en el paso 2 de este artículo nos permitía utilizar estilos para definir el aspecto del control, los comandos nos permiten utilizar estilos para definir el modelo de interacción del control. Todos los controles que admiten enlace a un comando se pueden utilizar para activar y desactivar el estado del comando. Se podría utilizar un hipervínculo, un botón o un control personalizado que se defina.

Eventos

Disponemos ya de un control bastante funcional, así que, por último, debemos ofrecer la capacidad para que el desarrollador atienda algunos eventos y puedan actuar sobre el control. En este caso, interesaría atender la expansión o contracción del control.

Actualmente, los eventos se definen mediante un evento CLR simple. Esto funciona perfectamente en numerosos eventos (y de hecho, funcionaría correctamente para el evento que definimos aquí), pero en ocasiones hay eventos que requieren una mayor funcionalidad. En Avalon, los controles se basan en la composición, por lo que en ocasiones se incluyen un gran número de controles que ofrecen el mismo evento pero sólo se desea atender a dicho evento en un sitio. Por ejemplo, se podría agregar un controlador de eventos a la ventana de la aplicación de ejemplo y se notificaría cuando se hiciera clic en cualquier botón de esa ventana:

<?Mapping XmlNamespace="local" ClrNamespace="InsideAvalon" ?>
<Window
  xmlns="http://schemas.microsoft.com/2003/xaml"
  xmlns:x="Definition"
  xmlns:l="local"
  x:Class="InsideAvalon.ExpanderTest"
  Background="Beige"
  Text="Inside Avalon"
  Button.Click="AnyButtonClicked"
  >
  ...
</Window>

public partial class ExpanderTest : Window
{
    void AnyButtonClicked(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Clicked!");
    }
}

Es un ejemplo poco usual, pero nos permite hacernos una idea. Para definir un nuevo evento redirigido, el nombre de los eventos que participan en el sistema de eventos de Avalon, se sigue un patrón similar al de las propiedades.

public class Expander : HeaderedContentControl
{
    public static RoutedEventID ExpandingEvent;
    public static Command ToggleExpandedCommand;
    public static DependencyProperty IsExpandedProperty;
    static Expander()
    {
        ExpandingEvent = EventManager.RegisterRoutedEventID(
            "Expanding",
            RoutingStrategy.Direct,
            typeof(RoutedEventHandler),
            typeof(Expander));
        ToggleExpandedCommand = new Command(
            "ToggleExpanded",
            typeof(Expander),
            null);
        IsExpandedProperty = DependencyProperty.Register(
            "IsExpanded",
            typeof(bool),
            typeof(Expander));
    }
    ...
}

RoutingStrategy determina el modo en que se redirigirá el evento alrededor del árbol de elementos. Los eventos directos sólo se activan en el elemento de origen (en este caso, el expansor), mientras que los eventos en burbuja se desplazan hacia arriba por el árbol de elementos (como el anterior ejemplo de clic) y los eventos en túnel se desplazan hacia abajo del árbol de elementos (cualquiera de los eventos de vista previa como el túnel PreviewKeyDown).

Con el fin de desencadenar el evento en el momento adecuado, se debe determinar cuándo se ha cambiado la propiedad. Para mayor simplicidad, desencadenaremos el evento de expansión siempre que la propiedad cambia a verdadero (true).

public class Expander : HeaderedContentControl
{
    ...
    protected override void OnPropertyInvalidated(
        DependencyProperty dp,
        PropertyMetadata metadata)
    {
        if (dp == IsExpandedProperty)
        {
            if (IsExpanded)
            {
                RoutedEventArgs e = new RoutedEventArgs();
                e.SetRoutedEventID(ExpandingEvent);
                this.RaiseEvent(e);
            }
        }
        base.OnPropertyInvalidated(dp, metadata);
    }
    ...
}

En realidad, este código se desencadena cuando el valor ha cambiado, ya que el evento se desencadena en el momento en que se obtiene una notificación invalidada. La invalidación puede ocurrir por varios motivos, aunque eso no implica en todos los casos que el valor de la propiedad se haya cambiado. Un ejemplo de esto sería el caso en que se cambia el estilo para un elemento. Los valores antiguos y nuevos pueden ser idénticos, pero como el origen de la propiedad se ha cambiado, el sistema de propiedad marcará la propiedad como invalidada. Si deseamos garantizar que el evento sólo se desencadene cuando cambie el valor, deberemos almacenar en caché el valor antiguo y el nuevo para compararlos dentro de la devolución de llamada invalidada.

Debido a un error en la versión CTP de Avalon, no se puede utilizar un evento en el marcado cuando éste se está compilando en ese mismo proyecto (recordemos por qué usamos el espacio de nombres local al principio de este artículo), en tal caso, deberemos enlazar el evento en el código.

...
<DockPanel>
  <l:Expander
    ID="_redExpander"
    DockPanel.Dock="Top"
    Header="Red Expander"
    IsExpanded="true">
    <Rectangle Height="75" Fill="Red" />
  </l:Expander>
  <l:Expander
    ID="_blueExpander"
    DockPanel.Dock="Top"
    Header="Blue Expander"
    IsExpanded="false">
    <Rectangle Height="75" Fill="Blue" />
  </l:Expander>
</DockPanel>
...

public partial class ExpanderTest : Window
{
    protected override void OnLoading(EventArgs args)
    {
        _redExpander.Expanding +=
            new RoutedEventHandler(ExpanderExpanding);
        _blueExpander.Expanding +=
            new RoutedEventHandler(ExpanderExpanding);
        base.OnLoading(args);
    }


    void ExpanderExpanding(object sender, RoutedEventArgs e)
    {
        ((Expander)e.OriginalSource).Header =
                     "Expanded at " + DateTime.Now;
    }
}

La ejecución del código, así como la expansión y contracción de los paneles da como resultado lo que se muestra en la figura 5.

Figura 5. Visualización de los paneles en expansión y contracción (paso 4 en el código de ejemplo)

Una vez más, con pasión

Después de ver los entresijos del ensamblaje de todas las piezas, podemos apreciar un proyecto más complejo. Mediante la definición de un estilo más estético, que contenga un estilo de vista de árbol, se puede crear un explorador de archivos o carpetas de pequeño tamaño utilizando únicamente el expansor que hemos generado. Esto se muestra en la figura 6 que aparece a continuación.

Figura 6. Un ejemplo más complejo y visual (paso 5 en el código de ejemplo)

Resumen

Al generar un control personalizado en Avalon, se seguirán normalmente estos pasos si se va a crear un control flexible que participe en el estilo.

  1. Decidir el modelo de contenido para el control (nosotros elegimos HeaderedContentControl para implementar nuestro encabezado y modelo de contenido).

  2. Generar el modelo de datos para el control (agregamos una propiedad booleana para controlar el estado expandido).

  3. Generar el modelo de interacción para el control (definimos un comando para activar y desactivar el estado expandido).

  4. Generar el modelo de programación que sea necesario (definimos el evento de expansión para habilitar el llenado dinámico del control).

  5. Proporcionar un aspecto interesante al control mediante estilo y árboles visuales.

En este artículo hemos visto cómo las propiedades, los comandos y los eventos nos permiten definir la forma de un control. Mediante estos tres conceptos, combinados con el modelo de contenido del control se pueden generar controles que se abstraen totalmente de la visualización. Mediante el uso de enlace a comandos, enlace a datos y la agregación de eventos se puede crear una visualización de controles totalmente nueva con el empleo únicamente del marcado.

Chris Anderson entró a formar parte del equipo de Microsoft en 1997 como desarrollador de Microsoft Visual Basic. Actualmente es arquitecto del equipo de Windows Client Platform que trabaja en Avalon. Es responsable del diseño, la experiencia de los desarrolladores y la arquitectura de los componentes de presentación de Windows. Últimamente se dedica a perfeccionar sus habilidades de patinaje y halo. La mujer de Chris tolera todas sus adicciones, incluida la fotografía digital, los blogs, los videojuegos y los equipos de cine en casa.

Mostrar: