Sadly, this might need a little background, so my apologies up front for the loquacity; here's what I have first: I have created a "MapCanvas" WPF control derived from Canvas that allows the user to draw and manipulate shapes (which it creates and manages as a list of Path instances). Users can draw, (multi-)select, rotate, scale, group, translate and clone such shapes, which display over an Image background hosted in the Canvas-derived control. The idea is to allow hotspots of interest to be drawn over the image of a map, with customisable data attached to their Tag properties. Everything can be translated or scaled (both the map Image and all the shapes drawn over it) using separate cached TranslateTransform, RotateTransform and ScaleTransform instances applied in a group to the Canvas' RenderTransform, so all the shapes and the underlying background image transform together. This all works fine.
What I want to do is to then move this MapCanvas control into another larger ongoing MVVM project as a View, and bind a ViewModel to it. Since the control has some fairly complex internal state to manage all the shape drawing and manipulation (and the usual fun and games with feeding keyboard and mouse events back from the host window to the canvas-derived control since Canvas is picky about when it will respond to them) there is quite a bit of code-behind in the control.
Ideally, yes, I know, that logic would live in the ViewModel and talk to the View through binding, but the Canvas needs to know about key, left & middle mouse up and down events as well as mouse movement to create and manipulate the Paths, and just getting those events to the control is not straightforward; I'm not in love with the code-behind solution, but it works. If anyone has a better idea, I'm all ears.
Anyway, I do at least want the Canvas' list of those Paths to be available to the ViewModel so that everything else in the larger project can be done with them properly through MVVM. Not too hard, I thought; expose the list of paths as a DependencyProperty. Since most of the changes involve simply adding to that list (that is, accessing but not assigning the list) I factored out methods for AddNewPathToList() and ClearPathList() which implement INotifyPropertyChanged so that the ViewModel will be advised if a path is added etc.
Then just bind a public property on the ViewModel back to the DependencyProperty on the View, right?
The code for the View (the MapCanvas control) is pretty long, so I won't post it. BTW if anyone is interested, I am happy to share that code; it works just fine, but is implemented with code-behind like an olde-fashioned Windows Forms control. I know that I am condemning my soul to the eternal fires for this. C'est la vie.
Control/View embedding in the main window:
<Window.DataContext>
<local:MapCanvasVM/>
</Window.DataContext>
<Grid>
<TextBlock Text="{Binding Path=ViewModelPathSelectionInfo}" Height="20" VerticalAlignment="Top"/>
<local:MapCanvas x:Name="mpcControl" ClipToBounds="True" Background="Gray" Margin="0,20,0,0" MultiSelectPath="{Binding Path=SelectedPaths, Mode=OneWayToSource}" NumSelectedPaths="{Binding Path=NumSelected, Mode=OneWayToSource}"/>
</Grid>
The TextBlock is just there for debugging; I can watch this in the View to see how many paths the ViewModel thinks there are selected.
The bindings are OneWayToSource since the View only ever tells the ViewModel what paths it currently has selected, never the other way; the paths are created and manipulated directly by the MapCanvas in response to mouse and key events. I know this isn't proper MVVM, but it was just too hard to even think about how to do all that. Instead, I just encapsulated everything into the control; you can drop it into a window and it works. To be honest, I've never tried MVVM in this direction (purely View to ViewModel) before, and I'm not even convinced it is the right way to go. But the project I want to drop this into will use these annotated maps as part of a much larger set of data already managed through MVVM.
Here are the DependencyProperties and modifier methods in the MapCanvas View:
public static readonly DependencyProperty MultiSelectPathProperty = DependencyProperty.Register("MultiSelectPath", typeof(List<Path>), typeof(MapCanvas));
public static readonly DependencyProperty NumSelectedPathsProperty = DependencyProperty.Register("NumSelectedPaths", typeof(int), typeof(MapCanvas));
and
protected void OnPropertyChanged(string PropertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
}
/// <summary>
/// Provides backing access to the MultiSelectPathProperty DependencyProperty exposing the list of currently selected Path instances; raises PropertyChanged for assignment but not changes through access; all modifying access should be made through AddToMultiSelectPath() or ClearMultiSelectPath() which also raise PropertyChanged once changes are made.
/// </summary>
public List<Path> MultiSelectPath
{
get
{
return (List<Path>)GetValue(MultiSelectPathProperty);
}
set
{
if (MultiSelectPath != value)
{
SetValue(MultiSelectPathProperty,value);
OnPropertyChanged("MultiSelectPath");
}
}
}
/// <summary>
/// Provides backing access to the NumSelectedPathsProperty DependencyProperty exposing the number of currently selected Path instances
/// </summary>
public int NumSelectedPaths
{
get { return (int)GetValue(NumSelectedPathsProperty); }
set
{
if (NumSelectedPaths != value)
{
SetValue(NumSelectedPathsProperty, value);
OnPropertyChanged("NumSelectedPaths");
}
}
}
/// <summary>
/// Adds a path to the multiselection and raises PropertyChanged to notify observers of that property that it has changed through access (normally only assignment triggers the event)
/// </summary>
/// <param name="NewPath"></param>
private void AddToMultiSelectPath(Path NewPath)
{
MultiSelectPath.Add(NewPath);
OnPropertyChanged("MultiSelectPath");
NumSelectedPaths = MultiSelectPath.Count;
}
/// <summary>
/// Clears the multiselection and raises PropertyChanged to notify observers of that property that it has changed through access (normally only assignment triggers the event)
/// </summary>
private void ClearMultiSelectPath()
{
MultiSelectPath.Clear();
OnPropertyChanged("MultiSelectPath");
NumSelectedPaths = MultiSelectPath.Count;
}
Updating the number of paths in the selection in the AddToMultiSelectPath(Path NewPath) or ClearMultiSelectPath() methods through the NumSelectedPaths setter raises the appropriate PropertyChanged event there, too, as you'd expect.
So far, so good.
Now here is the ViewModel, and in the comments you'll see why I'm so dumbfounded:
internal class MapCanvasVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private List<Path> mSelectedPaths = new List<Path>();
private int mNumSelected;
public MapCanvasVM()
{ }
private void OnPropertyChanged(string PropertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
}
public List<Path> SelectedPaths
{
get { return mSelectedPaths; }
set
{
if (mSelectedPaths != value)
{
mSelectedPaths = value;
// Huh ?!
// a breakpoint in this setter fires twice with new list (MapCanvas initialiser) then NULL (from InitializeComponent()?),
// but then with an empty list for the first time a path is added to the selection, then never again.
// BUT THE UNDERLYING PROPERTY IS CORRECTLY SET AT ALL TIMES.
// how is that even possible???
}
}
}
/// <summary>
/// Bound from VM back to a TextBlock on the View for debugging
/// </summary>
public string ViewModelPathSelectionInfo
{
get { return SelectedPaths.Count.ToString(); }
}
/// <summary>
/// Bound from View to VM
/// </summary>
public int NumSelected
{
get { return mNumSelected; }
set
{
if (mNumSelected != value)
{
mNumSelected = value;
// we don't actually care about this value, but knowing that it has changed means we know that the
// SelectedPaths property has also been updated, since for some reason that setter isn't behaving itself.
OnPropertyChanged("ViewModelPathSelectionInfo"); // let the View know we want to update the count of paths in the debugging TextBlock
var Dummy = SelectedPaths; // What??? SelectedPaths is CORRECTLY SET if I break here!!!
}
}
}
}
Well, no. That doesn't work, but its doesn't work in the strangest way, as noted in the comments above. Well, actually, it does seem to work, but I don't trust it an inch.
You can see that to test the bindings, I created an integer-valued NumSelectedPathsProperty DependencyProperty on the View that simply exposed the number of selected paths in the control's list (which is itself the other List-valued MultiSelectPathProperty DependencyProperty). When I bound NumSelectedPaths to the NumSelected property in the ViewModel using
NumSelectedPaths="{Binding Path=NumSelected, Mode=OneWayToSource}"
it works perfectly.
So why can I pass an integer from the View to the ViewModel but virtually identical code to pass a List using exactly the same setup has such weird behaviour?
I mean, it works, but stepping the code when I add a path to the selection in the MapCanvas only ever trips the setter once (with the wrong value) and never breaks again, even though breaking on the OTHER bound property and examining the runtime value of SelectedPaths shows it has the correct value every time, even though a breakpoint on the setter doesn't get hit.
So, finally, the question:
a) has anyone ever seen anything like this, or have any idea what is going on with the bindings behaving like that?
b) surely there is a better way to do this MVVM binding between the control and the ViewModel, yes? Advice?
c) in general, to get state from the View back to the ViewModel (not the other way, as virtually all the tutorials, code snippets and articles I've found explain) is there a general approach that is different to and better than what I've used here?
Thank you all so much in advance for reading all that, and for any kind words of advice.