DarioSantarelli.Blog("UgiDotNet");

<sharing mode=”On” users=”*” />
posts - 176, comments - 144, trackbacks - 3

My Links

News


This is my personal blog. These postings are provided "AS IS" with no warranties, and confer no rights.

logo linkedIn logo Twitter logo FaceBook logo RSS logo Email

Logo DotNetMarche
Logo XPUG Marche



Tag Cloud

Archives

Post Categories

My English Blog

giovedì 25 agosto 2011

[WPF] Drag and Drop con Rx

Le Reactive Extensions (Rx) del framework .NET sono un set di API dedicate a ciò che nella letteratura prende il nome di “reactive programming”. Rx nasce dalla distinzione di due scenari:
  • pull: un programma agisce estraendo dati dall’ambiente in modo interattivo (es. un’iterazione su una sequenza di dati prelevati dall’ambiente).
  • push: un programma reagisce ai dati che l’ambiente “spinge” verso di esso (es. un event-handler è un esempio di reazione in uno scenario push)

Data questa distinzione, Rx si basa sulla dualità tra il pattern Iterator e il pattern Observer. In breve, il pattern Observer in uno scenario push è analogo al pattern Iterator in uno scenario pull. Nel pattern Iterator, infatti, l’Iterator è l’attore che accede all'insieme degli elementi di un contenitore in modo interattivo. Il pattern Observer può essere interpretato in modo equivalente se si considera che l’ambiente genera spontaneamente i singoli elementi e li spinge verso il programma, ad esempio attraverso eventi. Quando uno stesso evento viene scatenato più volte, di fatto viene prodotta una sequenza di elementi di tipo “evento”, il che rende ovvia a questo punto la relazione con il pattern Iterator. Rx è stato introdotto quindi per mettere a disposizione degli sviluppatori un modello di programmazione push-based, duale rispetto al ben più conosciuto pull-based, pur non aggiungendo nulla di nuovo rispetto a quest’ultimo. Semplicemente, il modello push-based offre modi diversi se non più semplici per approcciare problemi comuni legati alla programmazione asincrona ed alla composizione di eventi.

Rx implementa il pattern Observer tramite le due interfacce IObserver<T> e IObservable<T>, corrispondenti a IEnumerator/IEnumerable del pattern Iterator. IObservable<T> costituisce una sorta di “collezione asincrona” di elementi (generati in momenti diversi quando si parla di eventi). La relazione tra IObservable<T> e IObserver<T> si trova nel fatto che ad un IObservable<T> è possibile registrare uno o più IObserver<T> (tramite il metodo IObservable<T>.Subscribe(IObserver<T> observer) ). IObserver<T> possiede tre metodi: OnNext(…), OnCompleted() e OnError(…). Essi vengono invocati dall’ IObservable<T> rispettivamente quando possiede nuovi dati, completa un’operazione e si verifica un errore.

Drag and Drop e LINQ-to-events

Nella pratica, uno dei (classici) casi in cui Rx mostra la sua potenza è nell’implementazione del Drag and Drop di un oggetto grafico. Infatti, a differenza delle comuni soluzioni event-based che intercettano eventi e sincronizzano variabili di stato con lo scopo di aggiornare correttamente la posizione dell’oggetto grafico, le Reactive Extensions consentono di convertire gli eventi .NET di interesse in sequenze osservabili che possiamo interrogare tramite query LINQ. Vediamo un esempio concreto. Uno dei classici approcci per implementare il drag and drop in WPF (come in Windows Froms o Silverlight) si basa sostanzialmente sulla sincronizzazione di tre eventi logici di un oggetto grafico: MouseDown, MouseUp e MouseMove. Nello specifico, in WPF, supponendo di avere il seguente XAML…

<Window x:Class="WebRequestWithRxSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Loaded="Window_Loaded">
    <Canvas>
        <StackPanel Name="myPanel">
            <Rectangle Fill="Maroon" Width="100" Height="100" />
        </StackPanel>
    </Canvas>
</Window>

… per gestire il drag and drop dello StackPanel “myPanel” potremmo sincronizzarne i tre eventi MouseLeftButtonDown, MouseLeftButtonUp e MouseMove in modo da gestirne il riposizionamento nel Canvas per ogni evento MouseMove compreso temporalmente tra un MouseLeftButtonDown ed un MouseLeftButtonUp. Quindi, ogni volta che si verifica l’evento MouseLeftButtonDown, per ogni successivo evento MouseMove aggiorniamo la posizione dello StackPanel all’interno del Canvas, tenendo conto della posizione del cursore relativa all’angolo in alto a sinistra dello StackPanel. Il tutto finché non viene scatenato l’evento MouseLeftButtonUp.

 

Point relativeMouseDownPosition = new Point();

myPanel.MouseLeftButtonDown += (s, e) =>

{

  if (myPanel.CaptureMouse())

  {

   relativeMouseDownPosition = e.GetPosition(myPanel);

  }

};

myPanel.MouseLeftButtonUp += (s, e) => { myPanel.ReleaseMouseCapture(); };

myPanel.MouseMove += (s, e) =>

{

  if (myPanel.IsMouseCaptured)

  {

    Point absoluteMousePosition = e.GetPosition(this);

    Point newPanelPosition = new Point(absoluteMousePosition.X - relativeMouseDownPosition.X,

                                       absoluteMousePosition.Y - relativeMouseDownPosition.Y);

    Canvas.SetLeft(myPanel, newPanelPosition.X);

    Canvas.SetTop(myPanel, newPanelPosition.Y);

  }

};

Il codice qua sotto implementa la stessa cosa tramite Rx. All’inizio la sintassi può sembrare un po’ meno chiara, ma con il tempo ci si accorge di come in realtà sia molto più leggibile e soprattutto manutenibile rispetto al codice precedente, in quanto si basa su un modello “push” query-based.

 

 var relativeMouseDownPositions = Observable.FromEventPattern<MouseButtonEventArgs>(myPanel, "MouseLeftButtonDown")
                                      .Do(evt => myPanel.CaptureMouse())
                                             .Select(evt => evt.EventArgs.GetPosition(myPanel));
 
 var mouseUpEvents = Observable.FromEventPattern<MouseButtonEventArgs>(myPanel, "MouseLeftButtonUp")
                           .Do(evt => myPanel.ReleaseMouseCapture());
 
 var absoluteMousePositions = Observable.FromEventPattern<MouseEventArgs>(myPanel, "MouseMove") 
.Select(evt => evt.EventArgs.GetPosition(this));
 
 IObservable<Point> dragPoints = from relativeMouseDownPosition in relativeMouseDownPositions
                                 from absoluteMousePosition in absoluteMousePositions.TakeUntil(mouseUpEvents)
                                 select new Point(absoluteMousePosition.X - relativeMouseDownPosition.X,
                                                  absoluteMousePosition.Y - relativeMouseDownPosition.Y); 
 

dragPoints.ObserveOn(
SynchronizationContext.Current).Subscribe(point =>
 {
    Canvas.SetLeft(myPanel, point.X);
    Canvas.SetTop(myPanel, point.Y);
 });  

Alcune considerazioni

  1. La prima cosa che salta all’occhio è come sia possibile convertire un evento .NET in una sequenza osservabile tramite il metodo Observable.FromEventPattern(…) , il quale permette di specificare il tipo di EventArgs dell’evento, l’oggetto che espone l’evento da convertire ed il nome dell’evento. In particolare, la seconda riga di codice denota come sia possibile convertire l’evento MouseLeftButtonUp dello StackPanel in una sequenza osservabile di tipo IObservable<EventPattern<MouseButtonEventArgs>>.
  2. Rx definisce un suo set di extension methods per comporre sequenze osservabili nello stesso modo in cui LINQ to Objects compone sequenze enumerabili. Ad esempio, il metodo Observable.TakeUntil<TSource, TOther>(…) ritorna tutti gli elementi della sequenza osservabile sorgente finché l’altra sequenza osservabile non produce un valore. Nel nostro esempio, questo extension method è stato usato per filtrare gli eventi MouseMove tra un evento MouseLeftButtonDown ed un MouseLeftButtonUp.
  3. Il metodo Observable.ObserveOn<TSource>(…) permette di fare in modo che le notifiche asincrone agli observer vengano effettuate su un thread o uno scheduler specificato. Nel mondo WPF, come in altre piattaforme che supportano un SynchronizationContext, per evitare eccezioni cross-thread quando si interagisce con la UI di solito si interagisce con il contesto di sincronizzazione tramite la chiamata ObserveOn(SynchronizationContext.Current) che in WPF in particolare è costituito da un oggetto di tipo DispatcherSynchronizationContext.

Note di installazione: se stiamo lavorando su Visual Studio 2010, il modo più veloce per integrare Rx nel nostro progetto è NuGet. Digitando “Rx” sulla textbox di ricerca dovremmo ottenere la seguente schermata. In particolare, il package Rx-Main referenzia l’assembly System.Reactive.dll mentre il package Rx-Testing referenzia l’assembly Microsoft.Reactive.Testing.dll.

 

posted @ giovedì 25 agosto 2011 16.27 | Feedback (2) | Filed Under [ .NET WPF ]

Powered by: