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!
Prima di scendere troppo in questa tana di coniglio. Hai esaminato 'MouseDragElementBehavior' e usando l'evento' UIElement.Drop'? –
Trovato questo [collegamento] (http://www.codeproject.com/Tips/105637/How-to-make-any-UI-element-drag-able-using-Behavio) da Chris point. posso aiutare. –
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