2016-04-19 34 views
6

Per gli ultimi due giorni sono stato bloccato su un problema (semplice?). Ho cercato molto su internet, ma non riesco a trovare un esempio che risolva completamente la situazione (ogni volta che viene escluso un aspetto, che è il fattore di rottura nella mia implementazione).WPF MVVM forme mobili con itemscontrol

Cosa voglio:

creare il mio controllo WPF, che visualizza un'immagine con in cima rettangoli (o forme in realtà in generale) che rimangono fissi, mentre lo zoom e panning. Inoltre, quei rettangoli devono essere ridimensionati (ancora) e possono essere spostati (in questo momento).

Desidero che questo controllo aderisca al modello di progettazione MVVM.

Cosa devo:

Ho un file XAML con un ItemsControl. Questo per mostrare una quantità dinamica di rettangoli (che provengono dal mio ViewModel). È legato ai RectItem del mio ViewModel (ObservableCollection). Voglio rendere gli oggetti come rettangoli. Questi rettangoli devono essere mobili da un utente usando il suo mouse. Durante lo spostamento dovrebbe aggiornare le entità del mio modello nel ViewModel.

XAML:

<ItemsControl ItemsSource="{Binding RectItems}"> 
<ItemsControl.ItemsPanel> 
     <ItemsPanelTemplate> 
      <Canvas IsItemsHost="True"> 

      </Canvas> 
     </ItemsPanelTemplate> 
    </ItemsControl.ItemsPanel> 
<ItemsControl.ItemContainerStyle> 
    <Style> 
     <Setter Property="Canvas.Left" Value="{Binding TopLeftX}"/> 
     <Setter Property="Canvas.Top" Value="{Binding TopLeftY}"/> 
    </Style> 
</ItemsControl.ItemContainerStyle> 
<ItemsControl.ItemTemplate> 
    <DataTemplate> 
     <Rectangle Stroke="Black" StrokeThickness="2" Fill="Blue" Canvas.Left="0" Canvas.Top="0" 
      Height="{Binding Height}" Width="{Binding Width}"> 
      <i:Interaction.Triggers> 
       <i:EventTrigger EventName="MouseLeftButtonUp"> 
        <i:InvokeCommandAction Command="{Binding ElementName=border,Path=DataContext.MouseLeftButtonUpCommand}" CommandParameter="{Binding}" /> 
       </i:EventTrigger> 
       <i:EventTrigger EventName="MouseLeftButtonDown"> 
        <i:InvokeCommandAction Command="{Binding ElementName=border,Path=DataContext.MouseLeftButtonDownCommand}" CommandParameter="{Binding}" /> 
       </i:EventTrigger> 
       <i:EventTrigger EventName="MouseMove"> 
        <i:InvokeCommandAction Command="{Binding ElementName=border,Path=DataContext.MouseMoveCommand}" CommandParameter="{Binding}" /> 
       </i:EventTrigger> 
      </i:Interaction.Triggers> 
     </Rectangle> 
    </DataTemplate> 
</ItemsControl.ItemTemplate> 

ViewModel:

public class PRDisplayViewModel : INotifyPropertyChanged 
{ 
    private PRModel _prModel; 

    private ObservableCollection<ROI> _ROIItems = new ObservableCollection<ROI>(); 

    public PRDisplayViewModel() 
    { 
     _prModel = new PRModel(); 

     ROI a = new ROI(); 
     a.Height = 100; 
     a.Width = 50; 
     a.TopLeftX = 50; 
     a.TopLeftY = 150; 

     ROI b = new ROI(); 
     b.Height = 200; 
     b.Width = 200; 
     b.TopLeftY = 200; 
     b.TopLeftX = 200; 

     _ROIItems.Add(a); 
     _ROIItems.Add(b); 

     _mouseLeftButtonUpCommand = new RelayCommand<object>(MouseLeftButtonUpInner); 
     _mouseLeftButtonDownCommand = new RelayCommand<object>(MouseLeftButtonDownInner); 
     _mouseMoveCommand = new RelayCommand<object>(MouseMoveInner); 
    } 

    public ObservableCollection<ROI> RectItems 
    { 
     get { return _ROIItems; } 
     set { } 
    } 

    private bool isShapeDragInProgress = false; 
    double originalLeft = Double.NaN; 
    double originalTop = Double.NaN; 
    Point originalMousePos; 

    private ICommand _mouseLeftButtonUpCommand; 

    public ICommand MouseLeftButtonUpCommand 
    { 
     get { return _mouseLeftButtonUpCommand; } 
     set { _mouseLeftButtonUpCommand = value; } 
    } 

    public void MouseLeftButtonUpInner(object obj) 
    { 
     Console.WriteLine("MouseLeftButtonUp"); 

     isShapeDragInProgress = false; 

     if (obj is ROI) 
     { 
      var shape = (ROI)obj; 
      //shape.ReleaseMouseCapture(); 
     } 
    } 


    /**** More Commands for MouseLeftButtonDown and MouseMove ****/ 

Classe ROI (questo sarebbe risiedere all'interno PRModel più tardi):

public class ROI 
{ 
    public double Height { get; set; } 

    public double TopLeftX { get; set; } 

    public double TopLeftY { get; set; } 

    public double Width { get; set; } 
} 

Così il mio modo di vedere è il seguente:

ItemsControl esegue il rendering di un oggetto ROI in un rettangolo. Gli eventi del mouse sul rettangolo sono gestiti dai comandi nel ViewModel. ViewModel, dopo aver ricevuto un evento del mouse, gestisce gli aggiornamenti direttamente sull'oggetto ROI. Quindi, la vista dovrebbe ridisegnare (assumendo che l'oggetto ROI sia cambiato), e quindi generare nuovi rettangoli, ecc.

Qual è il problema?

Nei gestori di eventi del mouse, devo chiamare il metodo CaptureMouse() del rettangolo su cui si è verificato l'evento del mouse. Come posso accedere a questo rettangolo?

Molto probabilmente il vero problema è che la mia prospettiva su MVVM qui è sbagliata. Dovrei effettivamente provare ad aggiornare gli oggetti ROI nei gestori di eventi del mouse in ViewModel? O devo aggiornare solo gli oggetti Rectangle? Se quest'ultimo, in che modo gli aggiornamenti si propagano agli oggetti ROI effettivi?

ho controllato molte altre domande, tra le quali quelle di seguito, ma io ancora non riescono a risolvere il mio problema:

Add n rectangles to canvas with MVVM in WPF

Dragable objects in WPF in an ItemsControl?


EDIT: Grazie tutti voi per le vostre risposte Il tuo contributo ha aiutato molto. Sfortunatamente, non riesco ancora a farlo funzionare (sono nuovo a WPF, ma non riesco a immaginare che sia così difficile).

Due nuovi tentativi, per i quali ho implementato l'interfaccia INotifyPropertyChanged nella classe ROI.

Tentativo 1: Ho implementato il trascinamento con MouseDragElementBehavior come questo:

   <ItemsControl ItemsSource="{Binding RectItems}"> 
       <ItemsControl.ItemsPanel> 
         <ItemsPanelTemplate> 
          <Canvas IsItemsHost="True"> 
          </Canvas> 
         </ItemsPanelTemplate> 
       </ItemsControl.ItemsPanel> 
       <ItemsControl.ItemContainerStyle> 
        <Style TargetType="ContentPresenter"> 
         <!-- //--> 
         <Setter Property="Canvas.Left" Value="{Binding TopLeftX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> 
         <Setter Property="Canvas.Top" Value="{Binding TopLeftY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> 
        </Style> 
       </ItemsControl.ItemContainerStyle> 
       <ItemsControl.ItemTemplate> 
        <DataTemplate> 
         <Grid> 
         <Rectangle Stroke="Black" StrokeThickness="2" Canvas.Left="0" Canvas.Top="0" 
          Height="{Binding Height}" Width="{Binding Width}"> 
           <i:Interaction.Behaviors> 
            <ei:MouseDragElementBehavior/> 
           </i:Interaction.Behaviors> 
          </Rectangle> 
         </Grid> 
        </DataTemplate> 
       </ItemsControl.ItemTemplate> 
      </ItemsControl> 

Che ha lavorato perfetto! Tutto è gratis, anche quando eseguo lo zoom su un bordo (che è un elemento principale di tutto questo).

problema qui: Dopo aver trascinato un rettangolo, lo vedo nell'interfaccia utente, ma il mio oggetto ROI (collegato a questo rettangolo) non viene aggiornato? Ho specificato l'associazione TwoWay con l'UpdateSourceTrigger PropertyChanged, ma ciò non funziona.

domanda qui: Come ottengo, in questa situazione, i miei oggetti ROI aggiornati? Posso implementare l'evento DragFinished di MouseDragElementBehavior, ma qui non ottengo l'oggetto ROI corrispondente? @Chris W., dove potrei associare un gestore UIElement.Drop? Posso ottenere l'oggetto ROI corrispondente lì?

Tentativo 2: Uguale al tentativo 1, ma ora ho implementato anche EventTrigger (come nel mio post originale). Lì, non ho fatto nulla per aggiornare il rettangolo nell'interfaccia utente, ma ho solo aggiornato l'oggetto ROI corrispondente.

problema qui: Questo non ha funzionato, perché i rettangoli abbiamo spostato "due volte" il vettore che dovrebbero. Probabilmente il primo movimento è dal trascinamento stesso, e il secondo movimento è l'aggiornamento manuale (da me) nelle ROI corrispondenti.

Tentativo 3: Invece di utilizzare MouseDragElementBehavior, "semplicemente" implementare il trascinamento utilizzando gli EventTriggers (come nel mio post originale). Non ho aggiornato alcun rettangolo (UI), ma solo le ROI (spostato il loro TopLeftX e TopLeftY).

problema qui: Questo ha funzionato, tranne nel caso dello zoom. Inoltre, il trascinamento non era bello, perché era un po '"tremolante", e mentre muoveva il mouse troppo velocemente, avrebbe perso il suo rettangolo. Chiaramente, in MouseDragElementBehavior, è stata posta una maggiore logica per rendere tutto più fluido.

@ Mark Feldman: grazie per la risposta. Non ha ancora risolto il mio problema, ma mi piace l'idea di non utilizzare le classi GUI nel mio ViewModel (come Mouse.GetPosition per implementare draggin da solo). Per disaccoppiare usando un convertitore implementerò in seguito, se la funzionalità funziona.

@Will: hai ragione (MVVM!= nessun codice dietro). Purtroppo al momento non vedo come posso sfruttarlo, poiché i gestori di eventi di MouseDragElementBehavior non conoscono una ROI (e questo mi servirebbe per aggiornare ViewModel).


EDIT 2:

Qualcosa che sta lavorando (almeno sembra così) è la seguente (usando MouseDragElementBehavior):

<ItemsControl ItemsSource="{Binding RectItems}"> 
       <ItemsControl.ItemsPanel> 
         <ItemsPanelTemplate> 
          <Canvas IsItemsHost="True"> 
          </Canvas> 
         </ItemsPanelTemplate> 
       </ItemsControl.ItemsPanel> 
       <ItemsControl.ItemContainerStyle> 
        <Style TargetType="ContentPresenter"> 

        </Style> 
       </ItemsControl.ItemContainerStyle> 
       <ItemsControl.ItemTemplate> 
        <DataTemplate> 
         <Grid> 
         <Rectangle Stroke="Black" StrokeThickness="2" Canvas.Left="0" Canvas.Top="0" 
          Height="{Binding Height}" Width="{Binding Width}"> 
           <i:Interaction.Behaviors> 
            <ei:MouseDragElementBehavior X="{Binding TopLeftX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Y="{Binding TopLeftY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> 

            </ei:MouseDragElementBehavior> 
           </i:Interaction.Behaviors> 
          </Rectangle> 
         </Grid> 
        </DataTemplate> 
       </ItemsControl.ItemTemplate> 
      </ItemsControl> 

ho legato la proprietà X e Y di MouseDragElementBehavior alla proprietà appropriata di ViewModel. Quando trascino il rettangolo, vedo i valori nella ROI corrispondente aggiornati! Inoltre, non c'è "sfarfallio" o altri problemi durante il trascinamento.

il problema ancora: Ho dovuto rimuovere il codice in ItemContainerStyle, utilizzato per inizializzare le posizioni dei rettangoli. Probabilmente un aggiornamento nel binding di MouseDragElementBehavior provoca anche qui un aggiornamento. Questo è visibile durante il trascinamento (il rettangolo salta rapidamente tra due posizioni).

domanda: utilizzando questo approccio, come è possibile inizializzare la posizione dei rettangoli? Inoltre, questo sembra un hack, quindi c'è un approccio più appropriato?


EDIT 3:

Il codice seguente sarà anche inizializzare i rettangoli sulla posizione appropriata:

<ItemsControl ItemsSource="{Binding RectItems}"> 
       <ItemsControl.ItemsPanel> 
         <ItemsPanelTemplate> 
          <Canvas IsItemsHost="True"> 
          </Canvas> 
         </ItemsPanelTemplate> 
       </ItemsControl.ItemsPanel> 
       <ItemsControl.ItemContainerStyle> 
        <Style TargetType="ContentPresenter"> 
         <!-- //--> 
         <Setter Property="Canvas.Left" Value="{Binding TopLeftX, Mode=OneTime}"/> 
         <Setter Property="Canvas.Top" Value="{Binding TopLeftY, Mode=OneTime}"/> 
        </Style> 
       </ItemsControl.ItemContainerStyle> 
       <ItemsControl.ItemTemplate> 
        <DataTemplate> 
         <Grid> 
         <Rectangle Stroke="Black" StrokeThickness="2" Canvas.Left="0" Canvas.Top="0" 
          Height="{Binding Height}" Width="{Binding Width}"> 
           <i:Interaction.Behaviors> 
            <ei:MouseDragElementBehavior X="{Binding TopLeftX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Y="{Binding TopLeftY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> 

            </ei:MouseDragElementBehavior> 
           </i:Interaction.Behaviors> 
          </Rectangle> 
         </Grid> 
        </DataTemplate> 
       </ItemsControl.ItemTemplate> 
      </ItemsControl> 

Problema ancora: Questo si sente 'hacky'. C'è un modo "migliore" per farlo (per ora sembra funzionare)?


EDIT 4: ho finito per usare Pollice. Uso di DataTemplates Sono stato in grado di definire l'aspetto di un oggetto dal mio ViewModel (così da usare, ad esempio Thumbs) e tramite ControlTemplates sono stato in grado di definire come dovrebbero essere effettivamente i pollici (un rettangolo, per esempio). Il vantaggio di Thumbs è che il trascinamento & di rilascio è già implementato!

+2

Prima di scendere troppo in questa tana di coniglio. Hai esaminato 'MouseDragElementBehavior' e usando l'evento' UIElement.Drop'? –

+0

Trovato questo [collegamento] (http://www.codeproject.com/Tips/105637/How-to-make-any-UI-element-drag-able-using-Behavio) da Chris point. posso aiutare. –

+1

MVVM! = No codebehind. L'interfaccia utente dovrebbe controllare ciò che accade nell'interfaccia utente, inclusi gli elementi in movimento. Il modo migliore per fare una sovrapposizione come questa sarebbe creare un controllo utente con una tela che si occupi di visualizzare e spostare i rettangoli. – Will

risposta

0

Bene, il tuo primo problema è che la ROI non supporta INotifyPropertyChange, quindi cambiarli non aggiornerà la tua grafica. Questo deve essere risolto per primo.

Con risposta al problema dell'evento, dare un'occhiata al mio progetto Perfy. Più in particolare, guarda la classe MouseCaptureBehavior che è responsabile per l'intercettazione dei messaggi del mouse e la loro confezione per il consumo dal modello di visualizzazione inclusa la fornitura delle funzionalità di acquisizione e rilascio. Nelle mie applicazioni più complesse creo un contratto tra la vista e vista del modello che si presenta in genere qualcosa di simile:

public interface IMouseArgs 
{ 
    Point Pos { get; } 
    Point ParentPos { get; } 
    void Capture(bool parent = false); 
    void Release(bool parent = false); 
    bool Handled { get; set; } 
    object Data { get;} 
    bool LeftButton { get; } 
    bool RightButton { get; } 
    bool Shift { get; } 
    void DoDragDrop(DragDropEffects allowedEffects); 
} 

ci sono un paio di cose da notare su questa interfaccia. Innanzitutto, il modello di visualizzazione non si preoccupa di quale sia l'implementazione, che rimane interamente alla vista. In secondo luogo, ci sono gestori di cattura/rilascio per il modello di vista da chiamare, sempre fornito dalla vista.Infine c'è un campo Dati, che in genere ho impostato per contenere il DataContext di qualunque oggetto è stato cliccato, utile per tornare alla vista per fargli sapere di quale oggetto (i) stai parlando.

Questo copre le cose dal lato del modello di vista, ora abbiamo bisogno di implementare questo lato della vista. Solitamente lo faccio con un trigger di evento che si collega direttamente ai gestori di comandi nel modello di visualizzazione ma utilizza un convertitore per prendere gli argomenti degli eventi specifici della vista e trasformarli in qualcosa che supporti l'oggetto IMouseArgs che il mio modello di visualizzazione si aspetta:

<!--- this is in the resources block --> 
<conv:MouseEventsConverter x:Key="ClickConverter" /> 

<!-- and this is in the ItemControl's ItemTemplate --> 
<ContentControl> 
    <i:Interaction.Triggers> 
     <i:EventTrigger EventName="PreviewMouseDown"> 
      <cmd:EventToCommand 
       Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:CanvasView}, 
       Path=DataContext.MouseDownCommand}" 
       PassEventArgsToCommand="True" 
       EventArgsConverter="{StaticResource ClickConverter}" 
       EventArgsConverterParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:SchedulePanel}}"/> 
     </i:EventTrigger> 
    </i:Interaction.Triggers> 
</ContentControl> 

Il legame stesso ha bisogno di qualche spiegazione ...

1) di comando. Questo legame viene eseguito sull'oggetto su cui l'utente fa clic (ad esempio, i tuoi rettangoli), ma il gestore dei comandi si troverà tipicamente nell'oggetto DataContext di ItemsControl principale. Quindi dobbiamo usare un RelativeBinding per trovare quell'oggetto e associarlo al suo gestore.

2) Path=DataContext.MouseDownCommand. Dovrebbe essere piuttosto semplice.

3) PassEventArgsToCommand="True". Questo dice a EventToCommand che il nostro gestore si aspetta un parametro di tipo MouseArgs. Potremmo fare ciò se volessimo, ma il nostro modello di visualizzazione potrebbe creare problemi con gli oggetti della GUI, quindi dobbiamo convertirlo nel nostro tipo IMouseArgs.

4) EventArgsConverter="{StaticResource ClickConverter}" Questo convertitore farà questa traduzione per noi, mostrerò il codice qui sotto.

5) EventArgsConverterParameter="... A volte abbiamo bisogno di passare altri dati nel nostro convertitore come parametro aggiuntivo. Non entrerò in tutti i casi specifici in cui ciò è necessario, ma tienilo presente. In questo caso specifico, avevo bisogno di trovare il punto relativo al genitore di ItemControl invece che allo stesso ItemControl.

La classe convertitore stesso può avere tutto quello che vuoi, una semplice implementazione sarebbe qualcosa di simile a questo:

public class MouseEventsConverter : IEventArgsConverter 
{ 
    public object Convert(object value, object parameter) 
    {   
     var args = value as MouseEventArgs; 
     var element = args.Source as FrameworkElement; 
     var parent = parameter as IInputElement; 
     var result = new ConverterArgs 
     { 
      Args = args, 
      Element = element, 
      Parent = parent, 
      Data = element.DataContext 
     }; 
     return result; 
    } 

Nota che la mia specifica implementazione qui (ConverterArgs) memorizza in realtà l'elemento quadro, si può sempre arrivare a con un cast se il modello di vista passa questo oggetto alla vista ovunque.

Ha senso? Sembra complesso, ma in realtà è abbastanza semplice una volta che lo hai codificato.