In diesem Wpf-Tutorial hab ich ja eine CollectionView vorgestellt, die die User-Selection hereinholt ins Viewmodel.
Hier jetzt gehe ich noch einen Schritt weiter, und verstoße richtig gegen den MVVM-Pattern: Ich hole mir nämlich ein Control selbst ins Viewmodel
. Ich hab da irgendwie keine Moral: wenn ich die Tastendrücke, die an ein Control gesendet werden, verarbeiten möchte, dann brauche ich den Eventhandler im Viewmodel.
Man kann da nun iwas umständliches reißen, mit einem Attached Behavior, wo das Event empfangen wird, und was dann iwas ans Viewmodel sendet - aber imo ist das nur eine verkomplizierende Verschleierung desselben: Nämlich, dass im Viewmodel Control-Events verarbeitet werden.
Also ich habe für solche Fälle ein Universal-AttachedBehavior - nenne ich VisualSubscribe, weils dazu da ist, dass im Viewmodel Visual-Events abonniert werden (subscribe).
Die Anwendung sieht ganz harmlos aus:
Im Viewmodel lungert dann eine Action(Of Object)-Property rum, an die das VisualSubscribe gebunden wird, und die genau dann genau einmal aufgerufen wird, wenn die Bindung erfolgt:
Also im Bindungs-Moment wird die Property aufgerufen, und kann dann das _ItemsControl setzen (meine .Be()-Extension übernimmt den notwendigen Cast). Und weils WithEvents ist, kann ich alle dessen Events verarbeiten
.
Aber vlt. sollte ich erstmal erzählen, wasses werden soll: Ich will eine Spielfläche haben, und darauf sollen Objekte herumfahren, deren Bewegungen vom User gesteuert werden.
Das Viewmodel der Objekte ist super-banal - einfach ein Dingens, was seine X/Y-Position angeben kann:
Class Moveable
Das Mainmodel nun enthält viele solcher Dingense in einer ObservableCollection:
Und man sieht auch, wieso ich die Tastendrücke haben will: nämlich um die Position des fokussierten Moveables (um StepSize) zu verändern (zeilen#23-#26).
Und wie ist im Xaml daran gebunden?
Tss! - einfach ein ItemsControl (also eine abgespeckte Listbox)!
Und im .ItemTemplate (#13-#17) ist an garnix gebunden - da wird einfach nur ein Standard-Button generiert, für jedes Moveable einer.
Aber das listige ist das .ItemsPanel (#2-#6): Das ist nämlich ein Canvas - also ein Panel, was die enthaltenen Controls nicht von oben nach unten aufreiht (wies bei einer Listbox normal wäre), sondern im Canvas wird jedem Control einzeln eine X/Y - Position zugewiesen, also Canvas.Left/.Top, um genau zu sein.
Dummerweise nützt es nix, die Buttons im DataTemplate zu positionieren, denn die sitzen gar nicht direkt auf dem Canvas auf, sondern die Buttons liegen jeweils in einem ItemContainer, und der liegt im Canvas.
Also muss der ItemContainer positioniert werden - nicht der Button im DataTemplate. Die ItemContainer (es sind ja viele - für jeden Button einer) sind beim ItemsControl vom Typ ContentPresenter, und beeinflussen kann man die nur über einen Style (#7-#12), den das ItemsControl dann auf jeden ItemContainer anwendet, wenn die Daten-Präsentation generiert wird.
Uff!!
Dann aber ist lustig, weil: (fast) fertig.
Also das ItemsControl ist funktional immer noch eigentlich eine Listbox, nur es sieht überhaupt nicht mehr wie eine Listbox aus, weil die Items darin ganz beliebig angeordnet sein können, und sich sogar bewegen!
Aber wir haben noch ein Problem zu lösen, nämlich die Z-Reihenfolge.
Denn natürlich soll ein Item nach vorne kommen, wenn mans fokussiert - wie geht das? Naja, im ItemsControl werden die Daten-Präsentationen genau in der Reihenfolge generiert, die die Auflistung im Viewmodel vorgibt - das letzte Item wird also zuoberst dargestellt. Also muss man im Viewmodel einfach das angeklickte Item nehmen und an die letzte Stelle verschieben - dann kommt im View seine Präsentation (hier: der Button) nach vorne - Code:
Das .GotFocus ist ein RoutedEvent, und zwar mit
.
Ja, und der weitere Code moved das Moveable an die letzte Position in der ObeservableCollection - ist nett, oder? - die hat extra eine Methode zum Verschieben von Elementen, und via Databinding benachrichtigt sie auch das ItemsControl.
Hier jetzt gehe ich noch einen Schritt weiter, und verstoße richtig gegen den MVVM-Pattern: Ich hole mir nämlich ein Control selbst ins Viewmodel

Man kann da nun iwas umständliches reißen, mit einem Attached Behavior, wo das Event empfangen wird, und was dann iwas ans Viewmodel sendet - aber imo ist das nur eine verkomplizierende Verschleierung desselben: Nämlich, dass im Viewmodel Control-Events verarbeitet werden.
Also ich habe für solche Fälle ein Universal-AttachedBehavior - nenne ich VisualSubscribe, weils dazu da ist, dass im Viewmodel Visual-Events abonniert werden (subscribe).
Die Anwendung sieht ganz harmlos aus:

Aber vlt. sollte ich erstmal erzählen, wasses werden soll: Ich will eine Spielfläche haben, und darauf sollen Objekte herumfahren, deren Bewegungen vom User gesteuert werden.
Das Viewmodel der Objekte ist super-banal - einfach ein Dingens, was seine X/Y-Position angeben kann:
VB.NET-Quellcode
- Imports System.ComponentModel
- Public Class Moveable : Inherits NotifyPropertyChanged
- Private _X As Double = 0
- Public Property X() As Double
- Get
- Return _X
- End Get
- Set(value As Double)
- ChangePropIfDifferent(value, _X, "X")
- End Set
- End Property
- Protected _Y As Double = 0
- Public Property Y() As Double
- Get
- Return _Y
- End Get
- Set(value As Double)
- ChangePropIfDifferent(value, _Y, "Y")
- End Set
- End Property
- End Class
Das Mainmodel nun enthält viele solcher Dingense in einer ObservableCollection:
VB.NET-Quellcode
- Imports System.ComponentModel
- Imports System.Collections.ObjectModel
- Public Class MainModel : Inherits MainModelBase(Of MainModel)
- Public Property StepSize As Double = 2
- Public Property Moveables As New ObservableCollection(Of Moveable)
- Public Property GetItemsControl As Action(Of Object) = Sub(obj) _ItemsControl.Be(obj)
- Private WithEvents _ItemsControl As ItemsControl
- Public Sub New()
- If IsProvisional Then
- _Moveables.Add({New Moveable, New Moveable With {.X = 33, .Y = 22}})
- Return
- End If
- Call 5.Times(Sub(i) Moveables.Add(New Moveable With {.X = i * 30, .Y = i * 15}))
- End Sub
- Private Sub _ItemsControl_PreviewKeyDown(sender As Object, e As KeyEventArgs) Handles _ItemsControl.PreviewKeyDown
- Dim focused = _Moveables.Last
- Select Case e.Key
- Case Key.Up : focused.Y -= StepSize
- Case Key.Right : focused.X += StepSize
- Case Key.Down : focused.Y += StepSize
- Case Key.Left : focused.X -= StepSize
- Case Else : Return
- End Select
- e.Handled = True
- End Sub
- End Class
Und wie ist im Xaml daran gebunden?
XML-Quellcode
- <ItemsControl ItemsSource="{Binding Path=Moveables}" hlp:VisualSubscribe.Subscribe="{Binding Path=GetItemsControl}" >
- <ItemsControl.ItemsPanel>
- <ItemsPanelTemplate>
- <Canvas/>
- </ItemsPanelTemplate>
- </ItemsControl.ItemsPanel>
- <ItemsControl.ItemContainerStyle>
- <Style TargetType="{x:Type ContentPresenter}">
- <Setter Property="Canvas.Left" Value="{Binding Path=X}"/>
- <Setter Property="Canvas.Top" Value="{Binding Path=Y}"/>
- </Style>
- </ItemsControl.ItemContainerStyle>
- <ItemsControl.ItemTemplate>
- <DataTemplate>
- <Button Content="ClickMe" />
- </DataTemplate>
- </ItemsControl.ItemTemplate>
- </ItemsControl>
Und im .ItemTemplate (#13-#17) ist an garnix gebunden - da wird einfach nur ein Standard-Button generiert, für jedes Moveable einer.
Aber das listige ist das .ItemsPanel (#2-#6): Das ist nämlich ein Canvas - also ein Panel, was die enthaltenen Controls nicht von oben nach unten aufreiht (wies bei einer Listbox normal wäre), sondern im Canvas wird jedem Control einzeln eine X/Y - Position zugewiesen, also Canvas.Left/.Top, um genau zu sein.
Dummerweise nützt es nix, die Buttons im DataTemplate zu positionieren, denn die sitzen gar nicht direkt auf dem Canvas auf, sondern die Buttons liegen jeweils in einem ItemContainer, und der liegt im Canvas.
Also muss der ItemContainer positioniert werden - nicht der Button im DataTemplate. Die ItemContainer (es sind ja viele - für jeden Button einer) sind beim ItemsControl vom Typ ContentPresenter, und beeinflussen kann man die nur über einen Style (#7-#12), den das ItemsControl dann auf jeden ItemContainer anwendet, wenn die Daten-Präsentation generiert wird.
Uff!!
Dann aber ist lustig, weil: (fast) fertig.
Also das ItemsControl ist funktional immer noch eigentlich eine Listbox, nur es sieht überhaupt nicht mehr wie eine Listbox aus, weil die Items darin ganz beliebig angeordnet sein können, und sich sogar bewegen!
Aber wir haben noch ein Problem zu lösen, nämlich die Z-Reihenfolge.
Denn natürlich soll ein Item nach vorne kommen, wenn mans fokussiert - wie geht das? Naja, im ItemsControl werden die Daten-Präsentationen genau in der Reihenfolge generiert, die die Auflistung im Viewmodel vorgibt - das letzte Item wird also zuoberst dargestellt. Also muss man im Viewmodel einfach das angeklickte Item nehmen und an die letzte Stelle verschieben - dann kommt im View seine Präsentation (hier: der Button) nach vorne - Code:
VB.NET-Quellcode
- Private Sub _ItemsControl_GotFocus(sender As Object, e As RoutedEventArgs) Handles _ItemsControl.GotFocus
- Dim focused = e.OriginContextAs(Of Moveable)()
- With _Moveables
- Dim i = .IndexOf(focused)
- Dim ubound = .Count - 1
- If i < 0 OrElse i = ubound Then Return
- .Move(i, ubound) 'move focused at last position
- End With
- End Sub
RoutingStrategy.Bubble
. Das heißt: wenn man auf einen der Item-Buttons klickst, empfängt zuerst der dieses Event. Aber er leitet es weiter, an seinen Parent, den ContentPresenter. Und der leitet es noch weiter ans Items-Control, und so kommts am Ende hier heraus. Und mit meiner e.OriginContextAs()-Extension kann ich den DataContext von e.OriginalSource abrufen - den DataContext des geklicksten Buttons - mit anderen Worten: dasjenige Moveable, auf dessen Daten-Präsentation geklickst wurde 
Ja, und der weitere Code moved das Moveable an die letzte Position in der ObeservableCollection - ist nett, oder? - die hat extra eine Methode zum Verschieben von Elementen, und via Databinding benachrichtigt sie auch das ItemsControl.
Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „ErfinderDesRades“ ()