Enero de 2018

Volumen 33, número 1

Plataforma universal de Windows: creación de una aplicación de línea de negocio con UWP

Por Bruno Sonnino

Un día, su jefe acude a usted con una nueva asignación: Debe crear una nueva aplicación de línea de negocio (LOB) para Windows. La web no es una opción y debe elegir la mejor plataforma para garantizar que la aplicación estará disponible durante 10 años más como mínimo.

Parece una elección difícil: Puede seleccionar Windows Forms o Windows Presentation Foundation (WPF), que son tecnologías maduras con numerosos componentes. Además, como desarrollador de .NET, tiene muchos conocimientos y experiencias con estas tecnologías. ¿Estarán disponibles dentro de 10 años? Creo que sí; no hay nada que indique que Microsoft deje de proporcionarlas en un futuro próximo. No obstante, además de ser un poco antiguas (Windows Forms es de 2001 y WPF se creó en 2006), estas tecnologías presentan otros problemas: Windows Forms no usa las tarjetas gráficas más recientes y está atascado en el mismo estilo antiguo y aburrido, salvo que use componentes extra o haga algunos trucos mágicos. WPF, aunque se sigue actualizando, no incluye las últimas mejoras del sistema operativo ni su SDK. Además, ninguna de estas tecnologías se puede implementar en Microsoft Store ni pueden usarlo: No existe ninguna implementación ni instalación o desinstalación fácil, detección y distribución internacional, etc. Puede usar Centennial para empaquetar las aplicaciones, pero no es la "opción auténtica".

Si investiga un poco más, descubrirá la Plataforma universal de Windows (UWP) para desarrollar para Windows 10 y ver que no tiene los inconvenientes de Windows Forms y WPF. Se está mejorando activamente y se puede usar sin ningún cambio en una amplia variedad de dispositivos, desde el diminuto Raspberry Pi hasta el enorme Surface Hub. No obstante, todo el mundo afirma que UWP sirve para desarrollar para aplicaciones pequeñas para tabletas y teléfonos Windows; no existen aplicaciones de LOB que usen UWP. Además, es muy difícil aprender y desarrollar: todos los patrones nuevos, el lenguaje XAML, el espacio aislado que limita lo que se puede hacer, etc.

Todo eso queda lejos de la realidad. Aunque en el pasado la plataforma UWP presentaba algunas limitaciones y podía resultar difícil de aprender y usar, esto ya no es así. Ha habido un desarrollo activo de nuevas características: con la nueva actualización Fall Creators Update (FCU) y su SDK puede usar incluso las API de .NET Standard 2.0, así como acceder a SQL Server directamente. Al mismo tiempo, las herramientas y los componentes nuevos pueden ofrecerle la mejor experiencia de desarrollo de nuevas aplicaciones. Windows Template Studio (WTS) es una extensión para Visual Studio, que proporciona un inicio rápido a una aplicación para UWP con todas las características. Asimismo, puede usar los componentes de Telerik para UWP gratis, para disfrutar de la mejor experiencia de usuario. No está mal, ¿no le parece? Ya tiene una nueva plataforma para el desarrollo de aplicaciones de LOB para UWP.

En este artículo, voy a desarrollar una aplicación de LOB para UWP mediante WTS, los componentes de Telerik y el acceso directo a SQL Server con .NET Standard 2.0, con la finalidad de mostrarle cómo funciona todo de primera mano.

Introducción a Windows Template Studio

Como dije antes, necesita FCU y el SDK correspondiente para usar .NET Standard 2.0. Una vez que tenga FCU instalado en la máquina (si no está seguro, presione Win+R y escriba Winver para ver la versión de Windows. La versión debería ser 1709 o más reciente). La compatibilidad de .NET Standard 2.0 en la plataforma UWP solo está disponible en Visual Studio 2017 actualización 4 o más reciente, por lo que si no ha instalado la actualización, debe hacerlo ahora. También debe tener el runtime de .NET 4.7 instalado para poder usarlo.

Una vez implementada la infraestructura, puede instalar WTS. En Visual Studio, vaya a Herramientas | Extensiones y actualizaciones, y busque Windows Template Studio. Después de la descarga, reinicie Visual Studio para completar la instalación. Para realizar la instalación, también puede ir a aka.ms/wtsinstall, descargar el instalador y, a continuación, instalar WTS.

Windows Template Studio es una nueva extensión de código abierto (el código fuente se puede obtener de aka.ms/wts) que le proporcionará una gran experiencia de inicio rápido al desarrollo con UWP: Puede elegir el tipo de proyecto [Navigation Pane (Panel de navegación) con el menú de hamburguesa, Blank (Proyecto en blanco) o Pivot and Tabs (Dinámico y pestañas)], el marco Model-View-ViewModel (MVVM) [Modelo–vista–modelo de vista (MVVM)] para su patrón MVVM, las páginas que quiere en el proyecto y las características de Windows 10 que desea incluir. En este proyecto se realiza una gran cantidad de desarrollo, y la compatibilidad con Xamarin, PRISM y las plantillas de Visual Basic ya están disponibles en las compilaciones nocturnas (que pronto se ofrecerán en la versión estable).

Con WTS instalado, puede ir a Visual Studio y crear un nuevo proyecto. Para ello, diríjase a Archivo | Nuevo proyecto | Windows Universal y seleccione Windows Template Studio. Se abrirá una nueva ventana para elegir el proyecto (consulte la Figura 1).

Pantalla principal de Windows Template Studio
Figura 1 Pantalla principal de Windows Template Studio

Seleccione el proyecto Navigation Pane (Panel de navegación) y el marco MVVM Light y, a continuación, haga clic en Next (Siguiente). Ahora, elija las páginas de la aplicación, como se muestra en la Figura 2.

Selección de página
Figura 2 Selección de página

Como puede ver en la columna Summary de la derecha, ya hay una página en blanco seleccionada, cuyo nombre es Main. Seleccione una página de Settings (Configuración) y asígnele el nombre Settings, y una página Master/Detail (Maestro-detalle) y asígnele el nombre Sales. A continuación, haga clic en el botón Next (Siguiente). En la ventana siguiente, puede seleccionar algunas características para la aplicación, como Live Tile (Icono dinámico) o Toast (Notificación del sistema), o bien un cuadro de diálogo First Use (Primer uso) que se presentará la primera vez que se ejecute la aplicación. No elegiré ninguna característica ahora, excepto Settings storage (Almacenamiento de configuración), que viene seleccionada al elegir la página Settings (Configuración).

Ahora, haga clic en el botón Create (Crear) para la crear la nueva aplicación con las características seleccionadas. Puede ejecutar la aplicación, que es una aplicación completa con un menú de hamburguesa, dos páginas (en blanco y maestro-detalle) y una página de configuración (a la que se accede haciendo clic en el icono de engranaje de la parte inferior), que permite cambiar los temas de la aplicación.

Si se dirige al Explorador de soluciones, verá que se crearon muchas carpetas para la aplicación, como Models, ViewModels y Services. También existe una carpeta Strings con una subcarpeta en-US para la localización de cadenas. Si quiere agregar más idiomas a la aplicación, solo tiene que agregar una subcarpeta a la carpeta Strings, asignarle el nombre de la configuración regional del idioma (por ejemplo, fr-FR), copiar el archivo Resources.resw y traducirla al nuevo idioma. Impresionante lo que ha podido hacer con unos pocos clics, ¿verdad? Estoy bastante seguro de que esto no es lo que su jefe tenía pensado cuando le asignó la tarea, así que vamos a personalizar la aplicación.

Acceso a SQL Server desde la aplicación

Una característica atractiva que se introdujo en la actualización FCU y en Visual Studio Update 4 es la compatibilidad con .NET Standard 2.0 en la plataforma UWP. Esta es una gran mejora, ya que permite que las aplicaciones para UWP usen un gran número de API que no estaban disponibles anteriormente, incluido el acceso de cliente de SQL Server y Entity Framework Core.

Para que el acceso de cliente de SQL Server esté disponible en su aplicación, diríjase a las propiedades de la aplicación y seleccione Compilación 16299 como Versión mínima en la pestaña de la aplicación. A continuación, haga clic en el nodo Referencias, seleccione "Administrar paquetes NuGet" e instale System.Data.SqlClient. Con esto ya está preparado para acceder a una base de datos de SQL Server local. Un apunte: el acceso no se lleva a cabo por medio de canalizaciones con nombre, que es el método de acceso predeterminado, sino mediante TCP/IP. Por tanto, debe ejecutar la aplicación Configuración de SQL Server y habilitar las conexiones TCP/IP para su instancia de servidor.

También usaré la interfaz de usuario de Telerik para la cuadrícula de UWP, un producto de código fuente actualmente gratuito que encontrará en bit.ly/2AFWktT. Por tanto, con la ventana del administrador de paquetes NuGet abierta, seleccione e instale el paquete Telerik.UI.for.UniversalWindowsPlatform. Si agrega una página de cuadrícula en WTS, este paquete se instala automáticamente.

Para esta aplicación, usaré la base de datos de ejemplo WorldWideImporters, que puede descargar de bit.ly/2fLYuBk y restaurar en su instancia de SQL Server.

Ahora, debe cambiar el acceso a datos predeterminado. Si se dirige a la carpeta Models, verá la clase SampleOrder, con este comentario al principio:

// TODO WTS: Remove this class once your pages/features are using your data.
// This is used by the SampleDataService.
// It is the model class we use to display data on pages like Grid, Chart, and Master Detail.

Existen muchos comentarios en todo el proyecto que ofrecen instrucciones sobre lo que debe hacer. En este caso, necesita una clase Order que es muy similar a esta. Cambie el nombre de la clase a Order y cámbiela por algo similar a lo de la Figura 3.

Figura 3 Clase Order

public class Order : INotifyPropertyChanged
{
  public long OrderId { get; set; }
  public DateTime OrderDate { get; set; }
  public string Company { get; set; }
  public decimal OrderTotal { get; set; }
  public DateTime? DatePicked { get; set; }
  public bool Delivered => DatePicked != null;
  private IEnumerable<OrderItem> _orderItems;
  public event PropertyChangedEventHandler PropertyChanged;
  public IEnumerable<OrderItem> OrderItems
  {
    get => _orderItems;
    set
    {
      _orderItems = value;
      PropertyChanged?.Invoke(
        this, new PropertyChangedEventArgs("OrderItems"));
    }
  }
  public override string ToString() => $"{Company} {OrderDate:g}  {OrderTotal}";
}

Esta clase implementa la interfaz INotifyPropertyChanged, porque quiero notificar a la interfaz de usuario cuándo los artículos del pedido cambian para poder cargarlos a petición cuando se muestre el pedido. Definí otra clase, OrderItem, para almacenar los artículos del pedido:

public class OrderItem
{
  public string Description { get; set; }
  public decimal UnitPrice { get; set; }
  public int Quantity { get; set; }
  public decimal TotalPrice => UnitPrice * Quantity;
}

La clase SalesViewModel, que se muestra en la Figura 4, también debe modificarse para reflejar estos cambios.

Figura 4 Clase SalesViewModel

public class SalesViewModel : ViewModelBase
{
  private Order _selected;
  public Order Selected
  {
    get => _selected;
    set
    {
      Set(ref _selected, value);
    }
  }
  public ObservableCollection<Order> Orders { get; private set; }
  public async Task LoadDataAsync(MasterDetailsViewState viewState)
  {
    var orders = await DataService.GetOrdersAsync();
    if (orders != null)
    {
      Orders = new ObservableCollection<Order>(orders);
      RaisePropertyChanged("Orders");
    }
    if (viewState == MasterDetailsViewState.Both)
    {
      Selected = Orders.FirstOrDefault();
    }
  }
}

Si la propiedad Selected se modifica, comprueba si los artículos del pedido ya están cargados y, si no es así, llama al método GetOrder­ItemsAsync del servicio de datos para cargarlos.

El último cambio que se debe llevar a cabo en el código es, en la clase SampleDataService, quitar los datos de ejemplo y crear el acceso de SQL Server, como se muestra en la Figura 5. Cambié el nombre de la clase por DataService, para reflejar que ya no es un ejemplo.

Figura 5 Código para recuperar los pedidos de la base de datos

public static async Task<IEnumerable<Order>> GetOrdersAsync()
{
  using (SqlConnection conn = new SqlConnection(
    "Database=WideWorldImporters;Server=.;User ID=sa;Password=pass"))
  {
    try
    {
      await conn.OpenAsync();
      SqlCommand cmd = new SqlCommand("select o.OrderId, " +
        "c.CustomerName, o.OrderDate, o.PickingCompletedWhen, " +
        "sum(l.Quantity * l.UnitPrice) as OrderTotal " +
        "from Sales.Orders o " +
        "inner join Sales.Customers c on c.CustomerID = o.CustomerID " +
        "inner join Sales.OrderLines l on o.OrderID = l.OrderID " +
        "group by o.OrderId, c.CustomerName, o.OrderDate,
          o.PickingCompletedWhen " +
        "order by o.OrderDate desc", conn);
      var results = new List<Order>();
      using (SqlDataReader reader = await cmd.ExecuteReaderAsync())
      {
        while(reader.Read())
        {
          var order = new Order
          {
            Company = reader.GetString(1),
            OrderId = reader.GetInt32(0),
            OrderDate = reader.GetDateTime(2),
            OrderTotal = reader.GetDecimal(4),
            DatePicked = !reader.IsDBNull(3) ? reader.GetDateTime(3) :
              (DateTime?)null
          };
          results.Add(order);
        }
        return results;
      }
    }
    catch
    {
      return null;
    }
  }
}
public static async Task<IEnumerable<OrderItem>> GetOrderItemsAsync(
  int orderId)
{
  using (SqlConnection conn = new SqlConnection(
        "Database=WideWorldImporters;Server=.;User ID=sa;Password=pass"))
  {
    try
    {
      await conn.OpenAsync();
      SqlCommand cmd = new SqlCommand(
        "select Description,Quantity,UnitPrice " +
        $"from Sales.OrderLines where OrderID = {orderId}", conn);
      var results = new List<OrderItem>();
      using (SqlDataReader reader = await cmd.ExecuteReaderAsync())
      {
        while (reader.Read())
        {
          var orderItem = new OrderItem
          {
            Description = reader.GetString(0),
            Quantity = reader.GetInt32(1),
            UnitPrice = reader.GetDecimal(2),
          };
          results.Add(orderItem);
        }
        return results;
      }
    }
    catch
    {
      return null;
    }
  }
}

El código es el mismo que usaría en cualquier aplicación de .NET; no cambia absolutamente nada. Ahora, debo modificar la plantilla de datos de elementos de lista en el archivo Sales­Page.xaml para reflejar los cambios, como se muestra en la Figura 6.

Figura 6 Plantilla de datos de elementos de lista

<DataTemplate x:Key="ItemTemplate" x:DataType="model:Order">
  <Grid Height="64" Padding="0,8">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
      <TextBlock Text="{x:Bind Company}" Style="{ThemeResource ListTitleStyle}"/>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{x:Bind OrderDate.ToShortDateString()}"
                   Margin="0,0,12,0" />
        <TextBlock Text="{x:Bind OrderTotal}" Margin="0,0,12,0" />
      </StackPanel>
    </StackPanel>
  </Grid>
</DataTemplate><DataTemplate x:Key="DetailsTemplate">
  <views:SalesDetailControl MasterMenuItem="{Binding}"/>
</DataTemplate>

Tengo que cambiar la propiedad DataType para que haga referencia a la nueva clase Order y cambiar los campos que se presentan en las clases TextBlock. También debo cambiar el código subyacente de SalesDetailControl.xaml.cs para cargar los artículos del pedido seleccionado cuando se modifique. Esto se lleva a cabo en el método OnMasterMenuItemChanged, que se transforma en un método asincrónico:

private static async void OnMasterMenuItemChangedAsync(DependencyObject d,
  DependencyPropertyChangedEventArgs e)
{
  var newOrder = e.NewValue as Order;
  if (newOrder != null && newOrder.OrderItems == null)
    newOrder.OrderItems = await
      DataService.GetOrderItemsAsync((int)newOrder.OrderId);
}

A continuación, debo cambiar SalesDetailControl para que apunte a los nuevos campos y los muestre, como se ilustra en la Figura 7.

Figura 7 Cambios en SalesDetailControl

<Grid Name="block" Padding="0,15,0,0">
  <Grid.Resources>
    <Style x:Key="RightAlignField" TargetType="TextBlock">
      <Setter Property="HorizontalAlignment" Value="Right" />
      <Setter Property="VerticalAlignment" Value="Center" />
      <Setter Property="Margin" Value="0,0,12,0" />
    </Style>
  </Grid.Resources>
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="Auto" />
    <RowDefinition Height="*" />
  </Grid.RowDefinitions>
  <TextBlock Margin="12,0,0,0"
    Text="{x:Bind MasterMenuItem.Company, Mode=OneWay}"
    Style="{StaticResource SubheaderTextBlockStyle}" />
  <StackPanel Orientation="Horizontal" Grid.Row="1" Margin="12,12,0,12">
    <TextBlock Text="Order date:"
      Style="{StaticResource BodyTextBlockStyle}" Margin="0,0,12,0"/>
    <TextBlock Text="{x:Bind MasterMenuItem.OrderDate.ToShortDateString(),
      Mode=OneWay}"
      Style="{StaticResource BodyTextBlockStyle}" Margin="0,0,12,0"/>
    <TextBlock Text="Order total:" Style="{StaticResource BodyTextBlockStyle}"
      Margin="0,0,12,0"/>
    <TextBlock Text="{x:Bind MasterMenuItem.OrderTotal, Mode=OneWay}"
      Style="{StaticResource BodyTextBlockStyle}" />
  </StackPanel>
  <grid:RadDataGrid ItemsSource="{x:Bind MasterMenuItem.OrderItems,
    Mode=OneWay}" Grid.Row="2"
    UserGroupMode="Disabled" Margin="12,0"
    UserFilterMode="Disabled" BorderBrush="Transparent"
      AutoGenerateColumns="False">
    <grid:RadDataGrid.Columns>
      <grid:DataGridTextColumn PropertyName="Description" />
      <grid:DataGridNumericalColumn
        PropertyName="UnitPrice"  Header="Unit Price"
        CellContentStyle="{StaticResource RightAlignField}"/>
      <grid:DataGridNumericalColumn
        PropertyName="Quantity"  Header="Quantity"
        CellContentStyle="{StaticResource RightAlignField}"/>
      <grid:DataGridNumericalColumn
        PropertyName="TotalPrice"  Header="Total Price"
        CellContentStyle="{StaticResource RightAlignField}"/>
    </grid:RadDataGrid.Columns>
  </grid:RadDataGrid>
</Grid>

Muestro los datos de ventas en la parte superior del control y uso un control RadDataGrid de Telerik para mostrar los artículos del pedido. Ahora, cuando ejecuto la aplicación, obtengo los pedidos en la segunda página, como se muestra en la Figura 8.

Aplicación que muestra pedidos de base de datos
Figura 8 Aplicación que muestra pedidos de base de datos

Sigo teniendo la página principal vacía. La usaré para mostrar una cuadrícula de todos los clientes. En MainPage.xaml, agregue la clase DataGrid a la cuadrícula de contenido:

<grid:RadDataGrid ItemsSource="{Binding Customers}"/>

Debe agregar el espacio de nombres a la etiqueta Page, pero no es necesario que recuerde la sintaxis y el espacio de nombres correcto. Solo tiene que colocar el cursor del mouse sobre RadDataGrid y presionar Ctrl+. para abrir un cuadro que le indicará el espacio de nombres correcto que se debe agregar. Como puede ver en la línea agregada, estoy enlazando la propiedad ItemsSource a Customers. La clase DataContext de la página es una instancia de MainViewModel, por lo que debo crear esta propiedad en MainViewModel.cs, como se muestra en la Figura 9.

Figura 9 Clase MainViewModel

public class MainViewModel : ViewModelBase
{
  public ObservableCollection<Customer> Customers { get; private set; }
  public async Task LoadCustomersAsync()
  {
    var customers = await DataService.GetCustomersAsync();
    if (customers != null)
    {
      Customers = new ObservableCollection<Customer>(customers);
      RaisePropertyChanged("Customers");
    }
  }
  public MainViewModel()
  {
    LoadCustomersAsync();
  }
}

Cargo los clientes cuando se crea la clase ViewModel. Una cosa a tener en cuenta aquí es que no espero a que se complete la carga. Cuando los datos están totalmente cargados, la clase ViewModel indica que la propiedad Customers ha cambiado y que carga los datos en la vista. El método GetCustomersAsync de la clase DataService es muy similar a GetOrdersAsync, como se puede ver en la Figura 10.

Figura 10 Método GetCustomersAsync

public static async Task<IEnumerable<Customer>> GetCustomersAsync()
{
  using (SqlConnection conn = new SqlConnection(
    "Database=WideWorldImporters;Server=.;User ID=sa;Password=pass"))
  {
    try
    {
      await conn.OpenAsync();
      SqlCommand cmd = new SqlCommand("select c.CustomerID,
        c.CustomerName, " +
        "cat.CustomerCategoryName, c.DeliveryAddressLine2, 
          c.DeliveryPostalCode, " +
        "city.CityName, c.PhoneNumber " +
        "from Sales.Customers c " +
        "inner join Sales.CustomerCategories cat on c.CustomerCategoryID =
          cat.CustomerCategoryID " +
        "inner join Application.Cities city on c.DeliveryCityID =
          city.CityID", conn);
      var results = new List<Customer>();
      using (SqlDataReader reader = await cmd.ExecuteReaderAsync())
      {
        while (reader.Read())
        {
          var customer = new Customer
          {
            CustomerId = reader.GetInt32(0),
            Name = reader.GetString(1),
            Category = reader.GetString(2),
            Address = reader.GetString(3),
            PostalCode = reader.GetString(4),
            City = reader.GetString(5),
            Phone = reader.GetString(6)
          };
          results.Add(customer);
        }
        return results;
      }
    }
    catch
    {
      return null;
    }
  }
}

Con eso, puede ejecutar la aplicación y ver los clientes en la página principal, como se muestra en la Figura 11. Con la cuadrícula Telerik, obtiene muchos extras gratis: las opciones de agrupación, ordenación y filtrado están integradas en la cuadrícula y no se tiene que realizar ninguna acción adicional.

Cuadrícula de clientes con agrupación y filtrado
Figura 11 Cuadrícula de clientes con agrupación y filtrado

Toques finales

Ahora, tengo una aplicación de LOB básica, con una cuadrícula de clientes y una vista de maestro-detalle que muestra los pedidos, pero puede usar algunos retoques. Los iconos de la barra lateral de ambas páginas son los mismos y se pueden personalizar. Estos iconos se establecen en la clase ShellViewModel. Si se dirige allí, verá estos comentarios, que apuntan a la ubicación donde se debe acudir para cambiar los iconos y el texto de los elementos:

// TODO WTS: Change the symbols for each item as appropriate for your app
// More on Segoe UI Symbol icons:
// https://docs.microsoft.com/windows/uwp/style/segoe-ui-symbol-font
// Or to use an IconElement instead of a Symbol see
// https://github.com/Microsoft/WindowsTemplateStudio/blob/master/docs/
projectTypes/navigationpane.md
// Edit String/en-US/Resources.resw: Add a menu item title for each page

Puede usar iconos de símbolo de fuente (como en el código real) o imágenes de otros orígenes si usa IconElements. (Puede usar archivos .png, rutas de acceso XAML o caracteres de cualquier otra fuente; eche un vistazo a bit.ly/2zICuB2 para obtener más información). Voy a usar dos símbolos de Segoe UI Symbol, People y ShoppingCart. Para hacerlo, debo cambiar el nodo NavigationItems en el código:

_primaryItems.Add(new ShellNavigationItem("Shell_Main".GetLocalized(),
  Symbol.People, typeof(MainViewModel).FullName));
_primaryItems.Add(new ShellNavigationItem("Shell_Sales".GetLocalized(),
  (Symbol)0xE7BF, typeof(SalesViewModel).FullName));

Para el primer elemento, ya existe un símbolo en la enumeración de Symbol, Symbol.People, pero para el segundo, no existe tal enumeración, por lo que uso el valor hexadecimal y lo convierto en la enumeración de Symbol. Para cambiar el título de la página y el del elemento de menú, edito Resources.resw y cambio Shell_Main y Main_Title.Text por Customers. Para agregar algo de personalización a la cuadrícula, puedo cambiar algunas propiedades:

<grid:RadDataGrid ItemsSource="{Binding Customers}" UserColumnReorderMode="Interactive"
                  ColumnResizeHandleDisplayMode="Always"
                  AlternationStep="2" AlternateRowBackground="LightBlue"/>

Adición de un icono dinámico a la aplicación

Para mejorar la aplicación, también puedo agregar un icono dinámico. Para hacerlo, debo dirigirme al Explorador de soluciones, hacer clic con el botón derecho en el nodo del proyecto y seleccionar Windows Template Studio | Nueva característica y, a continuación, elegir Icono dinámico. Al hacer clic en el botón Siguiente, mostrará los iconos afectados (los nuevos y los que se han modificado), para que pueda ver si le gusta realmente lo que hizo. Al hacer clic en el botón Finalizar, los cambios se agregan a la aplicación.

Se incluirá todo lo necesario para agregar un icono dinámico, solo tiene que crear el contenido del icono. Esto ya está hecho en LiveTileService.Samples. Es una clase parcial que agrega el método SampleUpdate a LiveTileService. Como se muestra en la Figura 12, cambiaré el nombre del archivo por LiveTileService.LobData y le agregaré dos métodos, Update­CustomerCount y UpdateOrdersCount, que mostrarán en los iconos dinámicos cuántos clientes y pedidos existen en la base de datos.

Figura 12 Clase para actualizar el icono dinámico

internal partial class LiveTileService
{
  private const string TileTitle = "LoB App with UWP";
  public void UpdateCustomerCount(int custCount)
  {
    string tileContent =
      $@"There are {(custCount > 0 ? custCount.ToString() : "no")}
        customers in the database";
    UpdateTileData(tileContent, "Customer");
  }
  public void UpdateOrderCount(int orderCount)
  {
    string tileContent =
      $@"There are {(orderCount > 0 ? orderCount.ToString() : "no")}
        orders in the database";
    UpdateTileData(tileContent,"Order");
  }
  private void UpdateTileData(string tileBody, string tileTag)
  {
    TileContent tileContent = new TileContent()
    {
      Visual = new TileVisual()
      {
        TileMedium = new TileBinding()
        {
          Content = new TileBindingContentAdaptive()
          {
            Children =
            {
              new AdaptiveText()
              {
                Text = TileTitle,
                HintWrap = true
              },
              new AdaptiveText()
              {
                Text = tileBody,
                HintStyle = AdaptiveTextStyle.CaptionSubtle,
                HintWrap = true
              }
            }
          }
        },
        TileWide = new TileBinding()
        {
          Content = new TileBindingContentAdaptive()
          {
            Children =
            {
              new AdaptiveText()
              {
                Text = $"{TileTitle}",
                HintStyle = AdaptiveTextStyle.Caption
              },
              new AdaptiveText()
              {
                Text = tileBody,
                HintStyle = AdaptiveTextStyle.CaptionSubtle,
                HintWrap = true
              }
            }
          }
        }
      }
    };
    var notification = new TileNotification(tileContent.GetXml())
    {
      Tag = tileTag
    };
    UpdateTile(notification);
  }
}

Inicialmente, se llamaba a UpdateSample durante la inicialización, en el método StartupAsync de ActivationService. Voy a reemplazarlo por el nuevo método UpdateCustomerCount:

private async Task StartupAsync()
{
  Singleton<LiveTileService>.Instance.UpdateCustomerCount(0);
  ThemeSelectorService.SetRequestedTheme();
  await Task.CompletedTask;
}

En este punto, todavía no tengo el recuento de clientes para actualizar. Esto sucederá cuando obtenga los clientes en MainViewModel:

public async Task LoadCustomersAsync()
{
  var customers = await DataService.GetCustomersAsync();
  if (customers != null)
  {
    Customers = new ObservableCollection<Customer>(customers);
    RaisePropertyChanged("Customers");
    Singleton<LiveTileService>.Instance.UpdateCustomerCount(Customers.Count);
  }
}

El recuento de pedidos se actualizará cuando tenga los pedidos en SalesViewModel:

public async Task LoadDataAsync(MasterDetailsViewState viewState)
{
  var orders = await DataService.GetOrdersAsync();
  if (orders != null)
  {
    Orders = new ObservableCollection<Order>(orders);
    RaisePropertyChanged("Orders");
    Singleton<LiveTileService>.Instance.UpdateOrderCount(Orders.Count);
  }
  if (viewState == MasterDetailsViewState.Both)
  {
    Selected = Orders.FirstOrDefault();
  }
}

Con esto, tengo una aplicación como la que se muestra en la Figura 13.

Aplicación finalizada
Figura 13 Aplicación finalizada

Esta aplicación puede mostrar los clientes y los pedidos recuperados de una base de datos local, y actualizar el icono dinámico con los recuentos de pedidos y clientes. Puede agrupar, ordenar o filtrar los clientes de la lista y mostrar los pedidos con una vista de maestro-detalle. No está mal.

Resumen

Como puede ver, la plataforma UWP no es solo para aplicaciones pequeñas. Puede usarla para sus aplicaciones de LOB, y obtener los datos de muchos orígenes, incluida una base de datos de SQL Server local (e incluso puede usar Entity Framework como un objeto ORM). Con .NET Standard 2.0, dispone de acceso a muchas API que ya están disponibles en .NET Framework, sin ningún cambio. WTS le proporciona un inicio rápido, que le ayudará a crear de manera rápida y sencilla una aplicación mediante los procedimientos recomendados y las herramientas que prefiera, así como a agregarle características de Windows. Existen excelentes controles de interfaz de usuario para mejorar el aspecto de la aplicación, y la aplicación se ejecutará en una gran variedad de dispositivos: teléfono, dispositivo de escritorio, Surface Hub e incluso HoloLens sin realizar ningún cambio.

En cuanto a la implementación, puede enviar la aplicación a la tienda y facilitar la detección internacional, la instalación y desinstalación sencillas y las actualizaciones automáticas. Si no quiere implementarla a través de la tienda, puede usar Internet (bit.ly/2zH0nZY). Como puede ver, si tiene que crear una nueva aplicación de LOB para Windows, sin duda debe considerar la posibilidad de usar UWP, ya que puede ofrecerle todo lo que quiere para este tipo de aplicación y más, con la gran ventaja de que se está desarrollando de manera muy activa y le proporcionará muchas mejoras durante los próximos años.


Bruno Sonnino es uno de los Profesionales más valiosos de Microsoft (MVP) desde 2007. Es desarrollador, consultor y autor de numerosos libros y artículos sobre el desarrollo de Windows. Puede seguirlo en Twitter: @bsonnino o leer sus entradas de blog en blogs.msmvps.com/bsonnino.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Clint Rutkas


Discuta sobre este artículo en el foro de MSDN Magazine