Januar 2018

Band 33, Nummer 1

Universelle Windows-Plattform: Erstellen einer Branchenanwendung mit der UWP

Von Bruno Sonnino

Eines Tages stellt Ihr Vorgesetzter Ihnen eine neue Aufgabe: Sie sollen eine neue Branchenanwendung (LOB-App) für Windows erstellen. Das Web ist keine Option, und Sie müssen die beste Plattform für die App auswählen, um sicherzustellen, dass sie für mindestens 10 weitere Jahre verfügbar ist.

Das scheint eine schwierige Entscheidung zu sein: Sie können sich für Windows Forms oder Windows Presentation Foundation (WPF) entscheiden. Dies sind ausgereifte Technologien mit zahlreichen Komponenten, und Sie als .NET-Entwickler verfügen über viel Wissen und Erfahrung im Umgang mit ihnen. Werden sie in 10 Jahren noch verwendet werden? Ich schätze, dass dies der Fall sein wird. Es gibt keine Anzeichen dafür, dass Microsoft in naher Zukunft eine dieser Technologien einstellen wird. Aber abgesehen davon, dass sie ein bisschen alt sind (Windows Forms stammt aus dem Jahr 2001, und WPF wurde 2006 erstellt), haben diese Technologien noch einige andere Probleme: Windows Forms verwendet nicht die aktuellen Grafikkarten, und Sie sind auf den gleichen alten langweiligen Stil beschränkt, es sei denn, Sie verwenden zusätzliche Komponenten oder setzen einige Zaubertricks ein. Und obwohl WPF immer noch aktualisiert wird, sind die neuesten Verbesserungen des Betriebssystems und seines SDK nicht integriert. Außerdem können beide Technologien nicht im Windows Store bereitgestellt werden und dessen Vorteile nutzen: Es gibt keine einfache Bereitstellung und Installation/Deinstallation, weltweite Erkennung und Verteilung usw. Sie können Centennial für die Paketerstellung von Apps verwenden, aber das reicht bei weitem nicht an die wirklichen Möglichkeiten heran.

Wenn Sie noch ein wenig tiefer graben, entdecken Sie die universelle Windows-Plattform (UWP) für die Entwicklung für Windows 10 und erkennen, dass diese nicht die Nachteile von Windows Forms und WPF aufweist. Sie wird aktiv verbessert und kann unverändert in einer Vielzahl von Geräten eingesetzt werden, vom winzigen Raspberry Pi bis hin zum riesigen Surface Hub. Aber alle behaupten, dass die UWP für die Entwicklung von kleinen Apps für Windows-Tablets und Smartphones gedacht ist. Es gibt keine LOB-Apps, die die UWP verwenden. Und abgesehen davon ist das Erlernen und Entwickeln sehr schwierig: all diese neuen Muster, die XAML-Sprache, der Sandkasten, der die möglichen Aktionen einschränkt. Und so weiter und so fort.

All das ist weit von der Realität entfernt. Obwohl die UWP in der Vergangenheit einige Einschränkungen aufwies und schwierig zu erlernen und zu verwenden sein konnte, ist dies nicht mehr der Fall. Die Entwicklung neuer Features wurde aggressiv vorangetrieben: Mit dem neuen Fall Creators Update (FCU) und seinem SDK können Sie sogar .NET Standard 2.0-APIs verwenden und direkt auf SQL Server zugreifen. Gleichzeitig können neue Tools und Komponenten Ihnen die beste Erfahrung bei der Entwicklung einer neuen App bieten. Windows Template Studio (WTS) ist eine Erweiterung für Visual Studio, die einen schnellen Einstieg in eine voll funktionsfähige UWP-App ermöglicht, und Sie können die Telerik-Komponenten für UWP kostenlos verwenden, um die bestmögliche UX bereitzustellen. Nicht schlecht, oder? Jetzt haben Sie eine neue Plattform für die Entwicklung Ihrer UWP LOB-Apps!

In diesem Artikel werde ich eine UWP LOB-App mit WTS, den Telerik-Komponenten und direktem Zugriff auf SQL Server mit .NET Standard 2.0 entwickeln, damit Sie zunächst sehen können, wie alles funktioniert.

Einführung in Windows Template Studio

Wie ich bereits sagte, benötigen Sie das FCU sowie das zugehörige SDK, um .NET Standard 2.0 verwenden zu können. Sobald Sie das FCU auf Ihrem Computer installiert haben (wenn Sie sich nicht sicher sind, drücken Sie WIN+R und geben „Winver“ ein, um Ihre Version von Windows anzuzeigen. Ihre Version sollte 1709 oder höher sein). Die Unterstützung für .NET Standard 2.0 in der UWP ist nur in Visual Studio 2017 Update 4 oder höher verfügbar. Wenn Sie also das Update noch nicht ausgeführt haben, muss dies jetzt geschehen. Sie müssen auch die .NET 4.7-Laufzeit installiert haben, um sie verwenden zu können.

Wenn die Infrastruktur vorhanden ist, können Sie WTS installieren. Navigieren Sie in Visual Studio zu „Extras“ > „Erweiterungen und Updates“, und suchen Sie nach „Windows Template Studio“. Nachdem Sie dieses Tool heruntergeladen haben, starten Sie Visual Studio neu, um die Installation abzuschließen. Sie können das Tool auch installieren, indem Sie zu aka.ms/wtsinstall navigieren, das Installationsprogramm herunterladen und dann WTS installieren.

Windows Template Studio ist eine neue Open Source-Erweiterung (Sie können den Quellcode von aka.ms/wts abrufen), die Ihnen einen unkomplizierten und schnellen Einstieg in die UWP-Entwicklung ermöglicht: Sie können den Typ des Projekts (ein Navigationsbereich mit dem Hamburger-Menü, ein leeres Projekt oder Pivot und Registerkarten), das MVVM-Framework (Model-View-ViewModel) für Ihr MVVM-Muster, die gewünschten Seiten in Ihrem Projekt und die Windows 10-Features, die Sie einbinden möchten, auswählen. In diesem Projekt findet eine Menge Entwicklung statt, und PRISM, Visual Basic-Vorlagen und Xamarin-Unterstützung sind bereits in den nächtlichen Builds verfügbar (demnächst in einer stabilen Version).

Wenn WTS installiert ist, können Sie Visual Studio öffnen und ein neues Projekt über „Datei“ > „Neues Projekt“ > „Windows/universell“ erstellen und „Windows Template Studio“ auswählen. Es wird ein neues Fenster geöffnet, in dem Sie das Projekt auswählen können (siehe Abbildung 1).

Hauptbildschirm von Windows Template Studio
Abbildung 1: Hauptbildschirm von Windows Template Studio

Wählen Sie das Projekt „Navigationsbereich“ und das MVVM Light-Framework aus, und klicken Sie dann auf „Weiter“. Wählen Sie nun die Seiten in der App aus, wie in Abbildung 2 gezeigt.

Seitenauswahl
Abbildung 2: Seitenauswahl

Wie Sie in der rechten Spalte „Zusammenfassung“ sehen können, ist bereits eine leere Seite mit dem Namen „Main“ ausgewählt. Wählen Sie eine Seite „Einstellungen“ aus, die Sie „Settings“ nennen, und eine Master/Detail-Seite, die Sie „Sales“ nennen. Klicken Sie dann auf die Schaltfläche „Weiter“. Im nächsten Fenster können Sie einige Features für die App auswählen, z.B. „Live-Kachel“ oder „Popup“, oder ein Dialogfeld „Erste Verwendung“, das bei der ersten Ausführung der App angezeigt wird. Ich werde jetzt keine Features auswählen (mit Ausnahme des Speichers „Settings“, der ausgewählt wird, wenn Sie die Seite „Einstellungen“ auswählen).

Klicken Sie nun auf die Schaltfläche „Erstellen“, um die neue App mit den ausgewählten Features zu erstellen. Sie können die App ausführen: Es handelt sich um eine vollständige App mit einem Hamburger-Menü, zwei Seiten (leer und Master/Detail) und einer Einstellungsseite (auf die durch Klicken auf das Zahnradsymbol am unteren Rand zugegriffen werden kann), die es Ihnen ermöglicht, die Designs für die App zu ändern.

Wenn Sie zum Projektmappen-Explorer navigieren, sehen Sie, dass viele Ordner für die Anwendung erstellt wurden, z.B. „Models“, „ViewModels“ und „Services“. Es ist sogar ein Ordner „Strings“ mit einem Unterordner „en-US“ für die Lokalisierung von Zeichenfolgen vorhanden. Wenn Sie der App weitere Sprachen hinzufügen möchten, müssen Sie dem Ordner „Strings“ nur einen neuen Unterordner hinzufügen, ihn anhand des Gebietsschemas der jeweiligen Sprache benennen (z.B. „de-DE“), die Datei „Resources.resw“ kopieren und sie dann in die neue Sprache übersetzen. Beeindruckend, was Sie mit wenigen Mausklicks erreicht haben, oder etwa nicht? Ich bin mir jedoch ziemlich sicher, dass das nicht das ist, was Ihr Vorgesetzter im Sinn hatte, als er Ihnen die Aufgabe zugewiesen hat, also lassen Sie uns diese App anpassen!

Zugreifen auf SQL Server aus der App

Ein nettes Feature, das im FCU und mit Visual Studio Update 4 eingeführt wurde, ist die Unterstützung von .NET Standard 2.0 in der UWP. Dies ist eine große Verbesserung, da UWP-Anwendungen nun eine große Anzahl von APIs verwenden können, die zuvor nicht verfügbar waren, einschließlich SQL Server-Clientzugriff und Entity Framework Core.

Um SQL Server-Clientzugriff in Ihrer App zur Verfügung zu stellen, navigieren Sie zu den Eigenschaften der App und wählen Build 16299 als Mindestversion auf der Registerkarte „Anwendung“ aus. Klicken Sie dann mit der rechten Maustaste auf den Knoten „Verweise“, wählen Sie „NuGet-Pakete verwalten“ aus, und installieren Sie „System.Data.SqlClient“. Auf diese Weise können Sie auf eine lokale SQL Server-Datenbank zugreifen. Ein Hinweis: Der Zugriff erfolgt nicht über Named Pipes (die Standardzugriffsmethode), sondern über TCP/IP. Daher müssen Sie die App „SQL Server-Konfiguration“ ausführen und TCP/IP-Verbindungen für Ihre Serverinstanz aktivieren.

Ich werde auch die Grid-Komponente der Telerik-Benutzeroberfläche für UWP verwenden, die nun ein kostenloses Open Source-Produkt ist, das Sie unter bit.ly/2AFWktT finden. Solange also das Fenster des NuGet-Paket-Managers noch geöffnet ist, wählen Sie das Paket „Telerik.UI.for.UniversalWindowsPlatform“ aus und installieren es. Wenn Sie eine Rasterseite in WTS hinzufügen, wird dieses Paket automatisch installiert.

Für diese App verwende ich die Beispieldatenbank „WorldWideImporters“, die Sie unter bit.ly/2fLYuBk herunterladen und in Ihrer SQL Server-Instanz wiederherstellen können.

Nun müssen Sie den Standarddatenzugriff ändern. Wenn Sie zum Ordner „Models“ navigieren, sehen Sie die Klasse „SampleOrder“ mit dem folgenden Kommentar am Anfang:

// 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.

Es gibt eine zahlreiche Kommentare im gesamten Projekt, die Hinweise dazu geben, wie Sie vorgehen müssen. In diesem Fall benötigen Sie eine Order-Klasse, die dieser Klasse sehr ähnlich ist. Benennen Sie die Klasse in „Order“ um, und ändern Sie sie so, dass sie Abbildung 3 ähnelt.

Abbildung 3: Die Order-Klasse

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}";
}

Diese Klasse implementiert die INotifyPropertyChanged-Schnittstelle, weil ich die Benutzeroberfläche benachrichtigen möchte, wenn die Auftragspositionen geändert werden, damit ich sie bei Bedarf bei der Anzeige des Auftrags laden kann. Ich habe eine weitere Klasse („OrderItem“) definiert, um die Auftragspositionen zu speichern:

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

Das „SalesViewModel“ (dargestellt in Abbildung 4) muss ebenfalls geändert werden, um diese Änderungen widerzuspiegeln.

Abbildung 4: „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();
    }
  }
}

Wenn die Selected-Eigenschaft geändert wird, wird überprüft, ob die Auftragspositionen bereits geladen sind. Wenn dies nicht der Fall ist, wird die Methode „GetOrderItemsAsync“ im Datendienst aufgerufen, um sie zu laden.

Die letzte Änderung im Code, die vorgenommen werden muss, erfolgt in der SampleDataService-Klasse, um die Beispieldaten zu entfernen und den SQL Server-Zugriff zu erstellen, wie in Abbildung 5 gezeigt. Ich habe die Klasse in „DataService“ umbenannt, um zu zeigen, dass es sich nicht mehr um ein Beispiel handelt.

Abbildung 5: Code zum Abrufen der Aufträge aus der Datenbank

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;
    }
  }
}

Der Code ist derselbe, den Sie in jeder beliebigen .NET-App verwenden würden.Es gibt keinerlei Änderungen. Jetzt muss ich die Datenvorlage für Listenelemente in „SalesPage.xaml“ so ändern, dass sie die Änderungen widerspiegelt, wie in Abbildung 6 gezeigt.

Abbildung 6: Die Datenvorlage für Listenelemente

<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>

Ich muss den „DataType“ so ändern, dass er sich auf die neue Order-Klasse bezieht, und die in den „TextBlocks“ dargestellten Felder ändern. Außerdem muss ich in „SalesDetailControl.xaml.cs“ CodeBehind ändern, um die Positionen für den ausgewählten Auftrag zu laden, wenn diese geändert werden. Dies geschieht in der OnMasterMenuItemChanged-Methode, die in eine asynchrone Methode transformiert wird:

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);
}

Als nächstes muss ich „SalesDetailControl“ so ändern, dass das Element auf die neuen Felder zeigt und diese anzeigt, wie in Abbildung 7 gezeigt.

Abbildung 7: Änderungen in „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>

Ich zeige die Verkaufsdaten im oberen Bereich des Steuerelements an und verwende ein Telerik-RadDataGrid, um die Auftragspositionen anzuzeigen. Wenn ich die Anwendung jetzt ausführe, erhalte ich die Aufträge auf der zweiten Seite, wie in Abbildung 8 gezeigt.

Anwendung, die Datenbankaufträge anzeigt
Abbildung 8: Anwendung, die Datenbankaufträge anzeigt

Ich verfüge immer noch über die leere Hauptseite. Ich werde sie verwenden, um ein Raster aller Kunden anzuzeigen. Fügen Sie in „MainPage.xaml“ das „DataGrid“ zum Inhaltsraster hinzu:

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

Sie müssen dem Page-Tag den Namespace hinzufügen, aber Sie müssen die Syntax und den richtigen Namespace nicht kennen. Positionieren Sie einfach den Mauszeiger über „RadDataGrid“, und geben Sie STRG+. ein. Es wird ein Feld geöffnet, das den richtigen Namespace angibt. Wie Sie aus der hinzugefügten Zeile ersehen können, binde ich die ItemsSource-Eigenschaft an „Customers“. Der „DataContext“ für die Seite ist eine Instanz von „MainViewModel“, daher muss ich diese Eigenschaft in „MainViewModel.cs“ erstellen, wie in Abbildung 9 gezeigt.

Abbildung 9: MainViewModel-Klasse

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();
  }
}

Ich lade die Kunden, wenn das „ViewModel“ erstellt wird. Hier ist zu beachten, dass ich nicht darauf warte, dass der Ladevorgang abgeschlossen ist. Wenn die Daten vollständig geladen wurden, zeigt das „ViewModel“ an, dass sich die Customer-Eigenschaft geändert hat. Dadurch werden die Daten in die Ansicht geladen. Die Methode „GetCustomersAsync“ in „DataService“ ist der GetOrdersAsync-Methode sehr ähnlich, wie Sie in Abbildung 10 sehen können.

Abbildung 10: GetCustomersAsync-Methode

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;
    }
  }
}

Damit können Sie die App ausführen und die Kunden auf der Hauptseite anzeigen, wie in Abbildung 11 gezeigt. Mit der Telerik-Grid-Komponente erhalten Sie viele kostenlose Annehmlichkeiten: Gruppierung, Sortierung und Filterung sind in die Grid-Komponente integriert, und es entsteht kein zusätzlicher Aufwand.

Kundenraster mit Gruppierung und Filterung
Abbildung 11: Kundenraster mit Gruppierung und Filterung

Abschließende Feinarbeiten

Ich verfüge jetzt über eine einfache LOB-Anwendung mit einem Kundenraster und einer Master/Detail-Ansicht, die die Aufträge anzeigt. Sie kann jedoch noch einige abschließende Feinarbeiten vertragen. Die Symbole in der seitlichen Leiste für beide Seiten sind gleich und können individuell angepasst werden. Diese Symbole werden in „ShellViewModel“ festgelegt. Wenn Sie dorthin navigieren, sehen Sie diese Kommentare, die darauf hinweisen, wo diese Symbole und der Text für die Elemente geändert werden:

// 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

Sie können Schriftsymbole (wie im eigentlichen Code) oder Bilder aus anderen Quellen verwenden, wenn Sie „IconElements“ verwenden. (Sie können PNG-Dateien, XAML-Pfade oder Zeichen aus beliebigen anderen Schriftarten verwenden. Weitere Informationen finden Sie unter bit.ly/2zICuB2.) Ich werde zwei Symbole aus Segoe UI Symbol verwenden („People“ und „ShoppingCart“). Zu diesem Zweck muss ich die „NavigationItems“ im Code ändern:

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

Für das erste Element ist bereits ein Symbol in der Symbolenumeration vorhanden („Symbol.People“), aber für das zweite Element gibt es keine solche Enumeration, also verwende ich den Hexadezimalwert und führe für ihn eine Typumwandlung in der Symbolenumeration aus. Um den Titel der Seite und die Beschriftung des Menüeintrags zu ändern, bearbeite ich „Resources.resw“ und ändere „Shell_Main“ und „Main_Title.Text“ in „Customers“. Ich kann auch einige Anpassungen am Raster vornehmen, indem ich einige Eigenschaften ändere:

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

Hinzufügen einer Live-Kachel zur App

Ich kann die Anwendung auch optimieren, indem ich eine Live-Kachel hinzufüge. Dazu navigiere ich zum Projektmappen-Explorer, klicke mit der rechten Maustaste auf den Projektknoten und wähle „Windows Template Studio“ > „Neues Feature“ und dann „Live-Kachel“ aus. Wenn Sie auf die Schaltfläche „Weiter“ klicken, werden die betroffenen Kacheln angezeigt (sowohl die neuen als auch die geänderten), sodass Sie sehen können, ob Ihnen die Änderungen wirklich gefallen. Wenn Sie auf die Schaltfläche „Fertig stellen“ klicken, werden die Änderungen der App hinzugefügt.

Alles, was Sie zum Hinzufügen einer Live-Kachel benötigen, ist vorhanden, Sie müssen nur den Inhalt der Kachel erstellen. Dies ist bereits in „LiveTileService.Samples“ geschehen. Dies ist eine Teilklasse, die „LiveTileService“ die SampleUpdate-Methode hinzufügt. Wie in Abbildung 12 gezeigt, benenne ich die Datei in „LiveTileService.LobData“ um und füge ihr zwei Methoden („UpdateCustomerCount“ und „UpdateOrdersCount“) hinzu, die in den Live-Kacheln anzeigen, wie viele Kunden oder Aufträge sich in der Datenbank befinden.

Abbildung 12: Klasse zum Aktualisieren der Live-Kachel

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);
  }
}

„UpdateSample“ wurde ursprünglich bei der Initialisierung in der StartupAsync-Methode von „ActivationService“ aufgerufen. Ich werde diese Angabe durch den neuen „UpdateCustomerCount“ ersetzen:

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

An diesem Punkt fehlt mir noch immer die zu aktualisierende Kundenzahl. Dies geschieht, wenn ich die Kunden in „MainViewModel“ abrufe:

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);
  }
}

Die Anzahl der Aufträge wird aktualisiert, wenn ich die Aufträge in „SalesViewModel“ abrufe:

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();
  }
}

Damit verfüge ich über eine App, wie sie in Abbildung 13 gezeigt wird.

Die fertige App
Abbildung 13: Die fertige App

Diese App kann die Kunden und Aufträge anzeigen, die aus einer lokalen Datenbank abgerufen wurden, und die Live-Kachel mit den Kunden und der Anzahl der Aufträge aktualisieren. Sie können die aufgelisteten Kunden gruppieren, sortieren oder filtern und die Aufträge in einer Master/Detail-Ansicht anzeigen. Nicht schlecht!

Zusammenfassung

Wie Sie sehen können, eignet sich die UWP nicht nur für kleine Apps. Sie können sie für Ihre LOB-Anwendungen verwenden, indem Sie die Daten aus vielen Quellen abrufen, z.B. auch aus einer lokalen SQL Server-Instanz (und Sie können Entity Framework sogar als ORM verwenden). Mit .NET Standard 2.0 verfügen Sie über Zugriff auf eine Vielzahl von APIs, die bereits in .NET Framework verfügbar sind, ohne dass Änderungen erforderlich wären. WTS bietet einen schnellen Einstieg und hilft Ihnen, mithilfe bewährter Methoden schnell und einfach eine Anwendung mit den von Ihnen bevorzugten Tools zu erstellen und ihr Windows-Features hinzuzufügen. Es sind großartige UI-Steuerelemente vorhanden, um das Erscheinungsbild der App zu verbessern, und die App kann auf einer Vielzahl von Geräten ausgeführt werden: auf einem Smartphone, einem Desktop, dem Surface Hub und sogar mit HoloLens ohne irgendwelche Änderungen.

Wenn die Bereitstellung ansteht, können Sie Ihre App an den Store senden und weltweite Erkennung, einfache Installation und Deinstallation sowie automatische Updates nutzen. Wenn Sie die Bereitstellung nicht über den Store vornehmen möchten, kann sie über das Web erfolgen (bit.ly/2zH0nZY). Wenn Sie eine neue LOB-App für Windows erstellen müssen, sollten Sie offensichtlich unbedingt die UWP in Betracht ziehen, weil sie Ihnen alles (und mehr) bieten kann, was Sie für diese Art von App benötigen. Dabei können Sie den großen Vorteil nutzen, dass die UWP aggressiv weiterentwickelt wird und in den nächsten Jahren zahlreiche Optimierungen bereitstellen wird.


Bruno Sonnino ist seit 2007 ein Microsoft Most Valuable Professional (MVP). Er arbeitet als Entwickler, Berater und Autor. Er hat zahlreiche Bücher und Artikel zur Windows-Entwicklung geschrieben. Folgen Sie ihm auf Twitter: @bsonnino, oder lesen Sie seine Blogbeiträge unter blogs.msmvps.com/bsonnino.

Unser Dank gilt dem folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Clint Rutkas


Diesen Artikel im MSDN Magazine-Forum diskutieren