x:Bind mit UWP und Performance

Mit Windows Universal Platform Apps wird XAML um eine Funktion erweitert, compiled Binding. Dazu muss man wissen, das in WPF Databinding per Reflection erst zur Laufzeit ausgewertet wird. Bekanntermaßen läuft man dabei leicht in Performanceprobleme rein. Da hört es sich doch super gut an, wenn nun mit X.Bind statt Binding alles schneller wird, oder?

Um diese Frage auch für meine Trainings Teilnehmer zu klären, wurde erst eine WPF MVVM Referenzimplementierung codiert und diese dann in einer Windows 10 App nachgebaut. Leider ist der Code nicht so kompatibel wie man glauben würde, so das das WPF Sample nochmals verändert wurde mit dem Ziel möglichst identen Code zu nutzen.

Das Testszenario füllt eine Listview mit einer Million Einträgen und misst die Zeit. In der WPF Anwendung dauert das 14 Sekunden.

image

Ich möchte nicht ausschließen, das in Betrachtungen Performance relevante Fakten nicht korrekt berücksichtigt worden sind und damit die Ergebnisse falsch sein können.

Das Viewmodel implementiert eine Counter Eigenschaft, weil UWP kein Liste.count im compiled Binding unterstützt. Außerdem scheint es in UWP Apps nötig zu sein einer Property vom Typ ObservableCollection manuell das PropertyChangedEvent auszulösen.

Public Class appVM
    Implements INotifyPropertyChanged
    Private _liste As ObservableCollection(Of zeichen)
    Public Property liste() As ObservableCollection(Of zeichen)
        Get
            Return _liste
        End Get
        Set(ByVal value As ObservableCollection(Of zeichen))
            _liste = value
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(liste)))
        End Set
    End Property
 
 
    Private _counter As Integer
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
 
    Public Property counter() As Integer
        Get
            Return _counter
        End Get
        Set(ByVal value As Integer)
            _counter = value
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(counter)))
        End Set
    End Property
End Class
 
Public Class zeichen
    Implements INotifyPropertyChanged
    Private _zeichen As String
    Public Property zeichen() As String
        Get
            Return _zeichen
        End Get
        Set(ByVal value As String)
            _zeichen = value
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(zeichen)))
        End Set
    End Property
 
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
End Class

Erstaunlicherweise ist .net zu schnell um das Bindung auch im Interface zu aktualisieren. Das heist erst wenn der folgende Code durchgelaufen ist, wird das UI refreshed.

   1:  Class MainWindow
   2:      Public Property l As New appVM
   3:      Private Sub button_Click(sender As Object, e As RoutedEventArgs) Handles button.Click
   4:          textBlock0.Text = Date.Now.ToLongTimeString
   5:          For i = 1 To 1000000
   6:              l.liste.Add(New zeichen With {.zeichen = Date.Now.ToLongTimeString})
   7:              l.counter = l.liste.Count
   8:          Next
   9:          textBlock1.Text = Date.Now.ToLongTimeString
  10:      End Sub
  11:   
  12:      Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
  13:          l.liste = New ObservableCollection(Of zeichen)
  14:          grid1.DataContext = l
  15:          l.liste.Add(New zeichen With {.zeichen = "test"})
  16:          l.counter = l.liste.Count
  17:      End Sub
  18:   
  19:  End Class

 

Im XAML Code wird lediglich eine Listview Control mit einem Datatemplate eingesetzt. Zu Testzwecken wurde mit dem Mode Oneway experimentiert (standard ist twoway) und Binding liste.count verwendet. Beides brachte keine sichtbare Änderung in der Durchlaufzeit.

   1:   <Grid x:Name="grid1">
   2:          <Button x:Name="button" Content="Button" HorizontalAlignment="Left" 
Margin="321,14,0,0" VerticalAlignment="Top" Width="75"/>
   3:          <TextBlock Text="{Binding counter}" HorizontalAlignment="Left" Width="100"
   4:                     TextWrapping="Wrap" VerticalAlignment="Top" Margin="407,18,0,0"/>
   5:          <ListView 
   6:              IsSynchronizedWithCurrentItem ="True"
   7:              ItemsSource="{Binding liste,Mode=OneWay }" x:Name="listView"
   8:              HorizontalAlignment="Left" Height="299" Margin="36,10,0,0" VerticalAlignment="Top" Width="158">
   9:              <ListView.ItemTemplate>
  10:                  <DataTemplate >
  11:                      <Grid>
  12:                          <TextBlock Text="{Binding zeichen,Mode=OneWay}"></TextBlock>
  13:                      </Grid>
  14:                  </DataTemplate>
  15:              </ListView.ItemTemplate>
  16:          </ListView>

 

Identes Szenario in der Windows 10 UPW App. XAML und VB.NET benötigen nun 48 Sekunden und damit mehr als 3x so lange.

image

Völlig überraschend reduziert sich die Zeit bei   Text="{Binding liste.count}"  statt   Text="{Binding counter}" auf 24 Sekunden. Mutmaßlich wird das Binding für den Counter so nur einmal aufgerufen.

Im nächsten Schritt führen wir compiled Bindung ein. Hier gibt es ein paar weitreichende Änderungen

  • Datacontext spielt keine Rolle mehr- Das Viewmodel wird als Property der Page zugewiesen
  • x : Bind ist per Default OneTime
  • Verschachtelte Bindings müssen per x : Datatype auflösbar sein.

Der XAML Code der WUP APP sieht damit wie folgt aus

   1:     xmlns:model="using:App4"
   2:  ...
   3:  <TextBlock Text="{x:Bind l.counter, Mode=OneWay}" HorizontalAlignment="Left" Margin="452,54,0,0" 
   4:                     TextWrapping="Wrap" VerticalAlignment="Top"/>
   5:    <ListView 
   6:         ItemsSource="{x:Bind l.liste,Mode=OneWay}" x:Name="listView"
   7:         HorizontalAlignment="Left" Height="299" Margin="36,10,0,0" VerticalAlignment="Top" Width="158">
   8:     <ListView.ItemTemplate>
   9:        <DataTemplate x:DataType="model:zeichen">
  10:            <Grid>
  11:              <TextBlock Text="{x:Bind zeichen, Mode=OneWay}"></TextBlock>
  12:            </Grid>
  13:       </DataTemplate>
  14:   </ListView.ItemTemplate>
  15:  </ListView>

 

Die Durchlaufzeit des kompilierten XAML Databinding Beispiels liegt nun bei 23 Sekunden und damit bei rund 50% des normalen Bindings. Allerdings noch immer deutlich über den Werten von WPF (14 Sekunden). Dazu kommt das sich X.Bind mit seinen default Wert überraschend verhält. Auch die Collection, die sich nicht aktualisiert, ist ein unerwartetes Ergebnis und Effekt des Kompilier Vorganges. Alternativ lässt sich mit Bindings.Update eine Aktualisierung erzwingen, was ich aber als seltsam im Kontext von MVVM empfinde. Zusätzlich war es schwierig Programmierfehler zu finden. Normale Binding Fehler werden im Output Window von Visual Studio angezeigt. Für compiled Bindings suche ich noch nach derartiger Hilfe. Was mich zusätzlich verwundert, ist das eigentlich eine UWP App native Code enthalten sollte und damit meines Erachtens nach schneller als eine .net JIT Optimierung sein sollte. Dieser Blog Artikel liefert also mehr Fragen als Antworten.

Nachtrag 1: Aufgrund erster Diskussionen beide Anwendungen direkt ohne Visual Studio gestartet. WPF: 12 Sekunden –70MB Ram im Taskmanager. WUP 10 21 Sekunden – 315 MB.

Kommentare sind geschlossen