June 2009 - Artículos

Si acabamos de instalar .NET RIA Services (CTP Mayo 2009) para disponernos a realizar nuestras primeras pruebas con esta tecnología de acceso a datos, pudiera ser que a la hora de intentar añadir al proyecto Web de la solución un servicio de dominio (DomainService), nos encontremos con que la plantilla correspondiente a este elemento, Domain Service Class, no está disponible en nuestro IDE de Visual Studio 2008.

 

En el caso de que esto nos ocurra, es posible que el origen del problema se encuentre motivado por el idioma configurado dentro del entorno de desarrollo. Si tenemos una versión de Windows en español, y dentro de la configuración internacional de Visual Studio 2008 (menú Herramientas > Opciones > Entorno > Configuración internacional), el apartado Idioma tiene el valor "Igual que en Microsoft Windows" o "español", es probable que este sea el motivo de tal comportamiento. 

 

Para intentar solucionarlo, en la lista desplegable Idioma cambiaremos el valor a "English", aceptaremos el diálogo, y reiniciaremos Visual Studio 2008. Abriendo nuevamente nuestro proyecto, al intentar agregar un nuevo Domain Service Class, ya debería de aparecer el icono correspondiente en el diálogo Add New Item. 

Pudiera ocurrir que el elemento "Domain Service Class" del diálogo Add New Item aparezca solamente con el texto descriptivo del elemento, sin el dibujo del icono, aunque esto no supondrá un problema, ya que podremos seleccionarlo igualmente para añadirlo al proyecto.

Espero que este truco resulte de ayuda a cualquiera que se pueda encontrar con este mismo problema.

Un saludo.

 

El planteamiento del problema

Durante el desarrollo de una aplicación ASP.NET, a la hora de diseñar la interfaz de usuario, podemos encontrarnos ante un escenario en el que se requiera que la navegación a ciertas páginas se centralice en un único punto de la aplicación.

En situaciones como esta, hacer uso de una página maestra resulta una excelente solución, ya que nos evita tener que duplicar el código en todas aquellas páginas que necesiten disponer de la mencionada funcionalidad de navegación. Adicionalmente, tendremos que incluir en dicha página maestra un control o controles que realicen la operación de navegación.

El control que en esta ocasión utilizaremos para el ejemplo a desarrollar será el TreeView de Silverlight 2, incluido en el Silverlight 2 Toolkit de marzo de 2009, cuyos pasos de descarga e instalación explicamos seguidamente.

 

Descarga e instalación del Silverlight 2 Toolkit

En caso de no disponer del Silverlight 2 Toolkit, podemos descargarlo en el siguiente enlace. Una vez completada la descarga, ejecutaremos su instalador, que a través de unos sencillos pasos, como vemos en la siguiente imagen, instalará el conjunto de controles que extienden las funcionalidades de Silverlight.

 

Finalizado el trabajo del instalador, en Visual Studio 2008 aparecerán, dentro de la caja de herramientas, los nuevos controles que forman parte del Toolkit (TreeView incluido, naturalmente).

   

Creando el proyecto Web y la página maestra

Como hemos indicado al comienzo del artículo, vamos a desarrollar una aplicación Web que estará compuesta por varios WebForm, los cuales harán uso de una página maestra, que albergará un control TreeView de Silverlight. El TreeView contendrá un conjunto de opciones (elementos TreeViewItem) en los que al hacer clic se producirá la redirección a la página correspondiente.

Comenzaremos pues, el desarrollo de nuestro ejemplo, iniciando Visual Studio 2008 y creando un nuevo proyecto Web con el nombre NavMasterPageTreeView, al que añadiremos una MasterPage con el nombre Maestra.Master. Posteriormente añadiremos el proyecto Silverlight a la solución, pero por ahora nos centraremos en los formularios Web.

 

 

Esta página maestra, como vemos en su código de presentación, estará compuesta por una tabla con dos celdas; una de ellas corresponderá a la zona que reservaremos para el contenido Silverlight, y la otra para los formularios Web a los que naveguemos dentro de la aplicación. 

 

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Maestra.master.cs" Inherits="NavMasterPageTreeView.Maestra" %>

<%@ Register Assembly="System.Web.Silverlight" Namespace="System.Web.UI.SilverlightControls"
    TagPrefix="asp" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <asp:ContentPlaceHolder ID="head" runat="server">
    </asp:ContentPlaceHolder>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    </asp:ScriptManager>
    <asp:UpdatePanel ID="UpdatePanel1" runat="server">
        <ContentTemplate>
            <div>
                <table border="7" width="100%">
                    <tr>
                        <td width="20%" align="center">
                            <%Espacio para contenido Silverlight%>
                        </td>
                        <td width="80%">
                            <asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
                                 <%Espacio que ocuparán los WebForms%>
                            </asp:ContentPlaceHolder>
                        </td>
                    </tr>
                </table>
            </div>
        </ContentTemplate>
    </asp:UpdatePanel>
    </form>
</body>
</html>

 

Los formularios de contenido

A continuación agregaremos al proyecto los diferentes formularios Web que utilizaremos para efectuar las operaciones de navegación. Dado que su contenido no representa un aspecto relevante en el desarrollo del ejemplo, estarán compuestos simplemente por un control Label  a modo de título de la página y un TextBox, al que en el evento Load, asignaremos como valor la hora en que se produjo la carga de la página; esto nos servirá para demostrar, más adelante, un curioso efecto que se produce al cargar el WebForm desde el TreeView.

En total serán cuatro los formularios que crearemos, los cuales vemos remarcados en la siguiente imagen del Explorador de soluciones de Visual Studio. 

 

Seguidamente podemos ver tanto el código de presentación como el code behind de uno de estos WebForm, obviamos el resto ya que comparten las mismas características.

 

<%@ Page Title="" Language="C#" MasterPageFile="~/Maestra.Master" AutoEventWireup="true"
    CodeBehind="Facturas.aspx.cs" Inherits="NavMasterPageTreeView.Facturas" %>

<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
    <table border="2" style="margin: 10px">
        <tr>
            <td>
                <asp:Label ID="Label1" runat="server" Text="FACTURAS"></asp:Label>
            </td>
            <td>
                <asp:TextBox ID="txtHora" runat="server"></asp:TextBox>
            </td>
        </tr>
    </table>
</asp:Content>

 

public partial class Facturas : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        this.txtHora.Text = DateTime.Now.ToString("HH:mm:ss");
    }
}

 

Creación del contenido Silverlight

El siguiente punto a desarrollar consiste en añadir el control TreeView a la aplicación y dotarle de la funcionalidad necesaria. Para ello, agregaremos a la solución con la que estamos trabajando un nuevo proyecto de tipo Silverlight con el nombre Opciones.

 

Cuando el asistente muestre el cuadro de diálogo para configurar la creación de este proyecto, desmarcaremos la casilla para la creación automática de la página de pruebas para el contenido Silverlight.

 

El motivo de no emplear una página de prueba reside en que nuestra página de testeo para el contenido Silverlight será la propia master page, tal y como indicábamos en un apartado anterior.

Situándonos en el editor de código XAML, diseñaremos una página que contenga un control TreeView, con varios elementos TreeViewItem dispuestos en dos niveles, que nos permitirán organizar por unos hipotéticos tipos de funcionalidad, las páginas de nuestra aplicación. No obstante, nos interesa visualizar en todo momento el árbol completo de opciones, por lo que en los TreeViewItem de nivel superior, asignamos el valor True a su propiedad IsExpanded. Todo ello lo vemos en el siguiente bloque de código.

 

<UserControl xmlns:controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls"  x:Class="Opciones.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="140" Height="170">
    <Grid x:Name="LayoutRoot" Background="White">
        <StackPanel>
            <controls:TreeView x:Name="tvOpciones" Width="130" Height="160" Margin="5">
                <controls:TreeViewItem Header="Gestión" IsExpanded="True">
                    <controls:TreeViewItem x:Name="tviFacturas" Header="Facturas"></controls:TreeViewItem>
                    <controls:TreeViewItem x:Name="tviListados" Header="Listados"></controls:TreeViewItem>
                </controls:TreeViewItem>
                <controls:TreeViewItem Header="Contabilidad" IsExpanded="True">
                    <controls:TreeViewItem x:Name="tviApuntes" Header="Apuntes"></controls:TreeViewItem>
                    <controls:TreeViewItem x:Name="tviBalances" Header="Balances"></controls:TreeViewItem>
                </controls:TreeViewItem>
            </controls:TreeView>
        </StackPanel>
    </Grid>
</UserControl>

 

El resultado visual quedará de la forma mostrada en la siguiente imagen.

 

 

Al hacer clic en cada uno de los TreeViewItem de nivel inferior, el resultado que pretendemos conseguir es la carga en el navegador del WebForm correspondiente; por lo tanto, como siguiente paso, debemos dotar a esta interfaz que acabamos de diseñar de la funcionalidad necesaria.

Abriendo el editor de code behind de la página XAML, escribiremos un manipulador para el evento Selected de los objetos TreeViewItem. Puesto que la operación a realizar es la misma en todos los casos, y únicamente cambiará la página a la que debemos navegar, solamente será necesario un único manipulador de evento, ya que tomaremos el valor de la propiedad Header del TreeViewItem que recibe el evento en su parámetro sender; concatenaremos a dicho valor la cadena ".aspx", y realizaremos la navegación mediante el método Navigate del objeto HtmlPage.Window. Dentro del constructor de la página Silverlight, efectuaremos la asociación entre el evento de los objetos TreeViewItem y el manipulador. Todo ello lo vemos en el siguiente bloque de código.

 

using Microsoft.Windows.Controls;
using System.Windows.Browser;
//....
public partial class Page : UserControl
{
    public Page()
    {
        //....
        this.tviFacturas.Selected += new RoutedEventHandler(tvi_Selected);
        this.tviListados.Selected += new RoutedEventHandler(tvi_Selected);
        this.tviApuntes.Selected += new RoutedEventHandler(tvi_Selected);
        this.tviBalances.Selected += new RoutedEventHandler(tvi_Selected);
    }
    //....
    void tvi_Selected(object sender, RoutedEventArgs e)
    {
        TreeViewItem tviOpcion = sender as TreeViewItem;
        string sNombreItem = tviOpcion.Header.ToString();
        HtmlPage.Window.Navigate(new Uri(sNombreItem + ".aspx", UriKind.Relative));
    }
}
 

Para completar la operación, en la master page del proyecto Web agregaremos un control Silverlight con el nombre slOpciones, cuya propiedad Source apunte al archivo xap del proyecto Silverlight que hemos desarrollado.

 

<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
    <ContentTemplate>
        <div>
            <table border="7" width="100%">
                <tr>
                    <td width="20%" align="center">
                        <asp:Silverlight ID="slOpciones" runat="server" 
                            Width="140px" Height="170px" 
                            Source="~/ClientBin/Opciones.xap" />
                    </td>
                    <td width="80%">
                        <asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
                        </asp:ContentPlaceHolder>
                    </td>
                </tr>
            </table>
        </div>
    </ContentTemplate>
</asp:UpdatePanel>
</form>

 

A continuación observamos el resultado en tiempo de ejecución.

  

El problema con el método HtmlPage.Window.Navigate

Sin embargo, como no podía ser de otro modo, y para hacer más interesante el ejemplo, ahora nos encontramos con un "pequeño" inconveniente, motivado por la mecánica de navegación que hemos implementado mediante el control TreeView.

El problema consiste en que después de haber cargado por primera vez uno de los formularios de la aplicación, haciendo clic en el elemento TreeViewItem correspondiente; en las sucesivas ocasiones en que volvamos a hacer clic en ese mismo TreeViewItem, el WebForm seleccionado no se cargará nuevamente, sino que se utilizará la versión cacheada de dicha página. Este comportamiento podemos comprobarlo muy fácilmente poniendo un punto de ruptura en el evento Load del WebForm, aunque en nuestro ejemplo no sería necesario porque observando el TextBox que contiene cada página, donde se muestra la hora de carga de la misma, nos percataremos de que en las siguientes ejecuciones a partir de la primera, la hora no se actualiza.

 

 

Una forma de solucionar esta contrariedad, de modo que se fuerce la carga de la página, consiste en utilizar el método HtmlPage.Window.Eval, al que pasaremos como parámetro una cadena conteniendo una sentencia en JavaScript que efectúe este cometido, tal y como vemos en el siguiente bloque de código.

 

void tvi_Selected(object sender, RoutedEventArgs e)
{
    TreeViewItem tviOpcion = sender as TreeViewItem;
    string sNombreItem = tviOpcion.Header.ToString();
    HtmlPage.Window.Eval("window.location = '" + sNombreItem + ".aspx ';");
}

 

En el siguiente post perteneciente al blog de Kevin Hazzard, se comenta también este comportamiento.

 

Resaltando visualmente el TreeViewItem seleccionado

Otro aspecto que también puede resultar problemático radica en el hecho de que al hacer clic en uno de los elementos del TreeView, no queda constancia visual del elemento seleccionado, ya que la navegación a una nueva página produce una recarga completa, contenido Silverlight incluido.

Lo deseable en este caso sería que el TreeViewItem pulsado fuera informado de la página que ha sido cargada en el navegador (página actual en ejecución), para que de alguna manera pudiera quedar remarcado visualmente

La técnica que vamos a seguir en nuestro ejemplo para conseguir esta funcionalidad pasa por hacer uso, desde el code behind de la master page, de la propiedad InitParameters perteneciente al control Silverlight alojado en esta página, lo que nos permitirá enviar, desde el lado servidor, información al contenido Silverlight.

Para asignar valores a InitParameters deberemos utilizar una cadena compuesta por pares de identificador=valor separados por coma, en un formato como el que vemos a continuación.

ControlSilverlight.InitParameters = "Identificador01=Valor01,Identificador02=Valor02..."; 

Esta operación la realizaremos en el evento Load de la página maestra, como vemos en el siguiente bloque de código. Para obtener la página que acaba de ser cargada recurriremos a la propiedad PhysicalPath del objeto Request.

 

public partial class Maestra : System.Web.UI.MasterPage
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string sPagina = Path.GetFileName(this.Request.PhysicalPath);
        this.slOpciones.InitParameters = "PaginaActual=" + sPagina.Replace(".aspx", string.Empty);
    }
}

 

La información asignada a InitParameters, será recogida en el proyecto Silverlight por el objeto Application que se halla en el código del archivo App.xaml.cs (en realidad se trata de una clase con el nombre App, que deriva de Application), más concretamente en el evento Startup de este objeto, que se desencadena al comienzo de la ejecución del contenido Silverlight.

El evento Startup recibe un parámetro de tipo StartupEventArgs, cuya propiedad InitParameters, de tipo IDictionary, utilizaremos para recuperar el valor de parámetro PaginaActual, que anteriormente establecimos en la página maestra. Dado que este valor necesitaremos utilizarlo desde el código de marcado de la página Silverlight (archivo Page.xaml), crearemos una propiedad en la clase App con el nombre PaginaActual, para poder manipularlo más fácilmente, como veremos brevemente.

 

public partial class App : Application
{
    string msPaginaActual;

    public string PaginaActual
    {
        get { return msPaginaActual; }
    }
    //....
    private void Application_Startup(object sender, StartupEventArgs e)
    {
        this.RootVisual = new Page();

        msPaginaActual = e.InitParams["PaginaActual"];
    }
    //....
}

 

Finalmente, en el code behind de la página Silverlight, escribiremos un manipulador para el evento Loaded, dentro de cuyo código, a partir de la propiedad estática App.Current, accederemos a la instancia del objeto App, y por ende a nuestra propiedad PaginaActual, que en función de su valor, nos servirá para resaltar el elemento TreeViewItem pertinente, utilizando su propiedad Background. 

 

public partial class Page : UserControl
{
    public Page()
    {
        InitializeComponent();

        this.Loaded += new RoutedEventHandler(Page_Loaded);
        //....
    }

    void Page_Loaded(object sender, RoutedEventArgs e)
    {
        SolidColorBrush scbItemSeleccionado = new SolidColorBrush(Colors.Cyan);
        App oAplicacion = App.Current as App;

        switch (oAplicacion.PaginaActual)
        {
            case "Facturas":
                this.tviFacturas.Background = scbItemSeleccionado;
                break;

            case "Listados":
                this.tviListados.Background = scbItemSeleccionado;
                break;

            case "Apuntes":
                this.tviApuntes.Background = scbItemSeleccionado;
                break;

            case "Balances":
                this.tviBalances.Background = scbItemSeleccionado;
                break;
        }
    }
}

 

La siguiente imagen muestra el resultado de esta característica que acabamos de añadir. 

 

 

Conclusiones

El empleo de una interfaz basada en una master page, que contenga elementos Silverlight tales como el control TreeView que hemos utilizado en el ejemplo, puede resultar de gran utilidad en el momento de desarrollar un mecanismo de navegación hacia las páginas Web que componen nuestra aplicación. Los ejemplos de código están disponibles en los siguientes enlaces: C# y VB. Espero que os pueda resultar de ayuda.

Un saludo.

Publicado por Luis Miguel Blanco | 8 comment(s)
Archivado en: ,