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.
I have a custom control inherited from Frame. (In a nutshell, it's kind of a custom alert box which I show over the whole app content).
I am using Custom Renderer to achieve this.
In xaml the control is located directly on the page (among other controls) (actually, I am creating it in the condebehind, but that makes no difference).
(Implementing it for iOS so far only).
The showing/hiding is initiated by IsVisible XF property. Then, I am adding the container view (native one) into the root of the app.
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsVisible")
{
// showing/hiding here
I am having two issues in this situation:
1. Right on this event raising the content positioning of the native view generated is not quite initialized: the iOS Frame values of the views don't have any widths/heights setup. That all probably done right after, so what I do is the following:
Task.Run(async () =>
{
await Task.Delay(10);
InvokeOnMainThread(() =>
{
SetupLayout();
Subviews[0].Alpha = 1;
SetupAnimationIn();
});
});
... which generally works, but still not quite slightly, and the approach is neither reliable nor nice.
On IsVisible = false it's even worse: I cannot handle the leaving animation as the element content got destroyed by XF engine (I suppose) right after (or even before) the notification raised, so the element disappears instantly, which doesn't look nice for the user experience.
So, is there any nice way to handle those things?
It's probably a little late, but I thought I would offer some guidance for anyone else trying to do something similar. For (1) I'm not quite sure what you mean that the native view is not quite initialized, but here is something you might find useful:
var measurement = Measure(screenWidth, screenHeight);
var contentHeight = measurement.Request.Height;
You can read more about what the 'Measure' method does in the docs, but basically it gives you the minimum dimensions of the view element based on its content. The arguments are constraints, so use the maximum size that the view might be. You can use it to initialize the dimensions manually by setting 'WidthRequest' and 'HeightRequest'.
(2) Basically you need to override the IsVisible property like this:
public static new readonly BindableProperty IsVisibleProperty = BindableProperty.Create(
nameof(IsVisible), typeof(bool), typeof(MyCustomView), default(bool), BindingMode.OneWay);
public new bool IsVisible
{
get => (bool)GetValue(IsVisibleProperty);
set => SetValue(IsVisibleProperty, value);
}
Note the use of the new keyword to override the inherited property. Of course, then you will need to handle the visibility yourself.
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == IsVisibleProperty.PropertyName)
{
if (IsVisible) AnimateIn();
else AnimateOut();
}
}
Now you can handle the visibility however you want. Hopefully this is of some help!
How do I get rid of this ugly line?
Draw a default bindingnavigator on an empty Form and you will see the problem. RenderMode is ManagerRenderMode. I want this render mode so the mouse over colors is correct. However, If I switch to System as rendermode the ugly line disapears, but then mouse over color/effect gets ugly.
I have been looking around for a solution for some time now, but nothing. Maybe someone here have seen this problem before?
It's not a BindingNavigator specific issue, but the ToolStrip which BindingNavigator inherits.
It's caused by the DrawToolStripBorder method when the ToolStripProfessionalRenderer class RoundedEdges property is true (the default).
In order to turn it off, I can suggest the following helper method:
public static class WindowsFormsExtensions
{
public static void DisableRoundedEdges(this ToolStripRenderer renderer)
{
var professionalRenderer = renderer as ToolStripProfessionalRenderer;
if (professionalRenderer != null)
professionalRenderer.RoundedEdges = false;
}
}
Now you can turn it off for the specific control (it's not available at design time, so it has to be at run time inside your form/control constructor or load event):
this.bindingNavigator1.Renderer.DisableRoundedEdges();
or to disable it globally, add the following in your Main method before calling Application.Run:
ToolStripManager.Renderer.DisableRoundedEdges();
I have a custom panel control, built in MainPage.xaml.cs, and I want it to redraw itself when the orientation changes (because it needs to measure the width of the display to look how I need it). I haven't found any way how to do this anywhere online :/
Declare this in your class
private SimpleOrientationSensor _orientationSensor;
then use it like this
_orientationSensor = SimpleOrientationSensor.GetDefault();
if (_orientationSensor != null)
{
_orientationSensor.OrientationChanged += delegate
{
// do whatever you need here
};
}
_orientationSensor must be member of class, otherwise will be collected by GC and event wont fire
How can I check if any given UIElement is currently visible on the UI?
There is the UIElement.Visibility property, but this is set by the progammer to indicate that the element should be hidden or visible.
I already check if the element is in the VisualTree.
All this does not help if there is another element on top that overlaps it.
WPF has a property UIElement.IsVisible that seems to do the job, but this is missing in Silverlight.
Any ideas?
Thanks
You can do some code to test the Visiblity and the HitTestVisible property of an element.
EDIT:
Try to do something like mentioned here,
Silverlight - Determine if a UIElement is visible on screen
There is a way to check the "render visibility" (although it cannot check for overlapping elements):
Elements do have the Unloaded event.
It is raised whenever changes to the VisualTree happen that result in the Element being part of a VisualTree branch that is not currently rendered. But luckily you don't have to listen for those events because only loaded elements do have decendants or a parent in the VisualTree.
And with the help of a nice little ExtensionMethod you can check whether an element is loaded or not at any given point in time:
public static class FrameworkElementExtensions
{
public static bool IsLoaded(this FrameworkElement element)
{
return element.GetVisualChildren().Any();
//I'm not sure if this alternative is better:
//return System.Windows.Media.VisualTreeHelper.GetParent(element)!= null;
//or
//return element.GetVisualParent() != null;
}
}