WPF Polyline databound to ObservableCollection Custom Control with MVVM - c#

I'm working towards making click and drag-able spline curves while learning WPF. I've been able to successfully work with pure Line segments, but making the jump to a polyline is proving difficult. I have a class for interpolating the spline curves that I used to use in WinForms, so I'm using a few input clicks from the mouse, and those will be the thumbs to click and drag. The interpolated points have a high enough resolution that a WPF Polyline should be fine for display. To clarify, I need the higher resolution output, so using a WPF Beizer is not going to work.
I have the outline pretty well setup- but the particular issue I'm having, is that dragging the thumbs does not either a) the two way binding is not setup correctly, or b) the ObservableCollection is not generating notifications. I realize that the ObservableCollection only notifies when items are added/removed/cleared, etc, and not that the individual indices are able to produce notifications. I have spent the last few hours searching- found some promising ideas, but haven't been able to wire them up correctly. There was some code posted to try inherit from ObservableCollection and override the OnPropertyChanged method in the ObservableCollection, but that's a protected virtual method. While others used a method call into the OC to attach PropertyChanged event handlers to each object, but I'm unsure where to inject that logic. So I am a little stuck.
MainWindow.xaml:
There is an ItemsControl hosted in a mainCanvas. ItemsControl is bound to a property on the ViewModel
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Menu>
<MenuItem x:Name="menuAddNewPolyline" Header="Add Polyline" Click="MenuItem_Click" />
</Menu>
<Canvas x:Name="mainCanvas" Grid.Row="1">
<ItemsControl x:Name="polylinesItemsControl"
ItemsSource="{Binding polylines}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</Grid>
MainWindow.Xaml.cs:
Pretty simple- initializes a new view model, and it's set as the DataContext. There is a menu with a Add Polyline item, which in turn, initializes a new PolylineControl, and generates three random points (using Thread.Sleep, otherwise they were the same, between the calls) within the ActualHeight and ActualWidth of the window. The new PolylineControl is added to the ViewModel in an ObservableCollection This is a stand in until I get to accepting mouse input.
public partial class MainWindow : Window
{
private ViewModel viewModel;
public MainWindow()
{
InitializeComponent();
viewModel = new ViewModel();
DataContext = viewModel;
}
private Point GetRandomPoint()
{
Random r = new Random();
return new Point(r.Next(0, (int)this.ActualWidth), r.Next(0, (int)this.ActualHeight));
}
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
var newPolyline = new PolylineControl.Polyline();
newPolyline.PolylinePoints.Add(GetRandomPoint());
Thread.Sleep(100);
newPolyline.PolylinePoints.Add(GetRandomPoint());
Thread.Sleep(100);
newPolyline.PolylinePoints.Add(GetRandomPoint());
viewModel.polylines.Add(newPolyline);
}
}
ViewModel.cs:
Absolutely noting fancy here
public class ViewModel
{
public ObservableCollection<PolylineControl.Polyline> polylines { get; set; }
public ViewModel()
{
polylines = new ObservableCollection<PolylineControl.Polyline>();
}
}
**The PolylineControl:
Polyline.cs:**
Contains DP's for an ObservableCollection of points for the polyline. Eventually this will also contain the interpolated points as well as the input points, but a single collection of points will do for the demo. I did try to use the INotifyPropertyChanged interface to no avail.
public class Polyline : Control
{
public static readonly DependencyProperty PolylinePointsProperty =
DependencyProperty.Register("PolylinePoints", typeof(ObservableCollection<Point>), typeof(Polyline),
new FrameworkPropertyMetadata(new ObservableCollection<Point>()));
public ObservableCollection<Point> PolylinePoints
{
get { return (ObservableCollection<Point>)GetValue(PolylinePointsProperty); }
set { SetValue(PolylinePointsProperty, value); }
}
static Polyline()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Polyline), new FrameworkPropertyMetadata(typeof(Polyline)));
}
}
Generic.xaml
Contains a canvas with a databound Polyline, and an ItemsControl with a DataTemplate for the ThumbPoint control.
<local:PointCollectionConverter x:Key="PointsConverter"/>
<Style TargetType="{x:Type local:Polyline}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Polyline}">
<Canvas Background="Transparent">
<Polyline x:Name="PART_Polyline"
Stroke="Black"
StrokeThickness="2"
Points="{Binding Path=PolylinePoints,
RelativeSource={RelativeSource TemplatedParent},
Converter={StaticResource PointsConverter}}"
>
</Polyline>
<ItemsControl x:Name="thumbPoints"
ItemsSource="{Binding PolylinePoints, RelativeSource={RelativeSource TemplatedParent}}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<tc:ThumbPoint Point="{Binding Path=., Mode=TwoWay}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
PointsCollectionConverter.cs:
Contains a IValueConverter to turn the ObservableCollection into a PointsCollection.
public class PointCollectionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.GetType() == typeof(ObservableCollection<Point>) && targetType == typeof(PointCollection))
{
var pointCollection = new PointCollection();
foreach (var point in value as ObservableCollection<Point>)
{
pointCollection.Add(point);
}
return pointCollection;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
And finally, the ThumbPointControl:
ThumbPoint.cs:
Contains a single DP for the center of the point, along with the DragDelta functionality.
public class ThumbPoint : Thumb
{
public static readonly DependencyProperty PointProperty =
DependencyProperty.Register("Point", typeof(Point), typeof(ThumbPoint),
new FrameworkPropertyMetadata(new Point()));
public Point Point
{
get { return (Point)GetValue(PointProperty); }
set { SetValue(PointProperty, value); }
}
static ThumbPoint()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ThumbPoint), new FrameworkPropertyMetadata(typeof(ThumbPoint)));
}
public ThumbPoint()
{
this.DragDelta += new DragDeltaEventHandler(this.OnDragDelta);
}
private void OnDragDelta(object sender, DragDeltaEventArgs e)
{
this.Point = new Point(this.Point.X + e.HorizontalChange, this.Point.Y + e.VerticalChange);
}
}
Generic.xaml:
Contains the style, and an Ellipse bound which is databound.
<Style TargetType="{x:Type local:ThumbPoint}">
<Setter Property="Width" Value="8"/>
<Setter Property="Height" Value="8"/>
<Setter Property="Margin" Value="-4"/>
<Setter Property="Background" Value="Gray" />
<Setter Property="Canvas.Left" Value="{Binding Path=Point.X, RelativeSource={RelativeSource Self}}" />
<Setter Property="Canvas.Top" Value="{Binding Path=Point.Y, RelativeSource={RelativeSource Self}}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ThumbPoint}">
<Ellipse x:Name="PART_Ellipse"
Fill="{TemplateBinding Background}"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Window after the Add Polyline menu item is pressed
The code works to add the polyline with three random points.
Thumbs moved away from poly line
However, once you move the thumbs, the polyline does not update along with it.
I have a working example of just a single line segment (added to the view model as many times as you click the add segment button) so it seems the logic should all be correct, but something broke down with the introduction of the ObservableCollection to host the multiple points required for a polyline.
Any help is appreciated

Following on from Clemens suggestions, I was able to make it work.
I renamed the Polyline.cs control to eliminate confusion with the standard WPF Polyline Shape class to DynamicPolyline. The class now implements INotifyPropertyChanged, and has DP for the PolylinePoints and a seperate ObservableCollection for a NotifyingPoint class which also implements INotifyPropertyChanged. When DynamicPolyline is initialized, it hooks the CollectionChanged event on the ObserableCollection. The event handler method then either adds an event handler to each item in the collection, or removes it based on the action. The event handler for each item simply calls SetPolyline, which in turn cycles through the InputPoints adding them to a new PointCollection, and then sets the Points property on the PART_Polyline (which a reference to is created in the OnApplyTemplate method).
It turns out the Points property on a Polyline does not listen to the INotifyPropertyChanged interface, so data binding in the Xaml was not possible. Probably will end up using a PathGeometery in the future, but for now, this works.
To address Marks non MVVM concerns.. It's a demo app, sorry I had some code to test things in the code behind. The point is to be able to reuse these controls, and group them with others for various use cases, so it makes more sense for them to be on their own vs repeating the code.
DynmicPolyline.cs:
public class DynamicPolyline : Control, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string caller = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
}
public static readonly DependencyProperty PolylinePointsProperty =
DependencyProperty.Register("PoilylinePoints", typeof(PointCollection), typeof(DynamicPolyline),
new PropertyMetadata(new PointCollection()));
public PointCollection PolylinePoints
{
get { return (PointCollection)GetValue(PolylinePointsProperty); }
set { SetValue(PolylinePointsProperty, value); }
}
private ObservableCollection<NotifyingPoint> _inputPoints;
public ObservableCollection<NotifyingPoint> InputPoints
{
get { return _inputPoints; }
set
{
_inputPoints = value;
OnPropertyChanged();
}
}
private void SetPolyline()
{
if (polyLine != null && InputPoints.Count >= 2)
{
var newCollection = new PointCollection();
foreach (var point in InputPoints)
{
newCollection.Add(new Point(point.X, point.Y));
}
polyLine.Points = newCollection;
}
}
private void InputPoints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems)
{
var point = item as NotifyingPoint;
point.PropertyChanged += InputPoints_PropertyChange;
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var item in e.OldItems)
{
var point = item as NotifyingPoint;
point.PropertyChanged -= InputPoints_PropertyChange;
}
}
}
private void InputPoints_PropertyChange(object sender, PropertyChangedEventArgs e)
{
SetPolyline();
}
public DynamicPolyline()
{
InputPoints = new ObservableCollection<NotifyingPoint>();
InputPoints.CollectionChanged += InputPoints_CollectionChanged;
SetPolyline();
}
static DynamicPolyline()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DynamicPolyline), new FrameworkPropertyMetadata(typeof(DynamicPolyline)));
}
private Polyline polyLine;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
polyLine = this.Template.FindName("PART_Polyline", this) as Polyline;
}
NotifyingPoint.cs
Simple class that raises property changed events when X or Y is updated from the databound ThumbPoint.
public class NotifyingPoint : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string caller = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
}
public event EventHandler ValueChanged;
private double _x = 0.0;
public double X
{
get { return _x; }
set
{
_x = value;
OnPropertyChanged();
ValueChanged?.Invoke(this, null);
}
}
private double _y = 0.0;
public double Y
{
get { return _y; }
set
{
_y = value;
OnPropertyChanged();
}
}
public NotifyingPoint()
{
}
public NotifyingPoint(double x, double y)
{
X = x;
Y = y;
}
public Point ToPoint()
{
return new Point(_x, _y);
}
}
And finally, for completeness, here is the Generic.xaml for the control. Only change in here was the bindings for X and Y of the NotifyingPoint.
<Style TargetType="{x:Type local:DynamicPolyline}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:DynamicPolyline}">
<Canvas x:Name="PART_Canvas">
<Polyline x:Name="PART_Polyline"
Stroke="Black"
StrokeThickness="2"
/>
<ItemsControl x:Name="PART_ThumbPointItemsControl"
ItemsSource="{Binding Path=InputPoints, RelativeSource={RelativeSource TemplatedParent}}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<tc:ThumbPoint X="{Binding Path=X, Mode=TwoWay}" Y="{Binding Path=Y, Mode=TwoWay}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I dropped my Spline class in to the SetPolyline method, and got the result I was after:
Two working click and drag able spline curves

Related

C# WPF MVVM, attached behavior to update mainwindow datacontext

In short: is it correct in MVVM pattern to access main window datacontext and update it through behavior class?
long: I'm trying to learn WPF MVVM and make app where one of the functionalities is canvas with draggable ellipses. I found few examples of behaviors that could provide this functionality but they relied on TranslateTransform and this was not the solution I wanted. I want to extract the ellipse coordinates for furhter use.
I also use ItemsControl to display canvas and related items which made impossible to use Canvas.SetTop() command.
After several tries I found a working solution but I’m not sure if this is correct according to MVVM pattern. And if this is the simplest way to achieve the goal… I take up coding as a hobby
if I made some concept mistakes please let me know.
Short app description:
On app startup the instance of TestWindow2VM class is crated and assigned to main window as datacontext
TestWindow2VM class contains ObservableCollection which contains EllipseVM class.
EllipseVM class holds X,Y coordinates and some other data (brushes etc).
In XAML in ItemsControl the binding of ItemsSource is set to my ObservableCollection. In ItemsControl Datatemplate I bind ellipse properties to data stored in EllipseVM class and also add reference to my behavior class
in ItemsControl ItemContainerStyle canvas top and left properties are bound to my ObservableCollection
when ellipse is clicked my behavior class access the datacontext, finds the instance of EllipseVM class and changes X and Y coordinates basing on mouse cursor position relative to canvas.
Code below:
behavior:
public class CanvasDragBehavior
{
private Point _mouseCurrentPos;
private Point _mouseStartOffset;
private bool _dragged;
private static CanvasDragBehavior _dragBehavior = new CanvasDragBehavior();
public static CanvasDragBehavior dragBehavior
{
get { return _dragBehavior; }
set { _dragBehavior = value; }
}
public static readonly DependencyProperty IsDragProperty =
DependencyProperty.RegisterAttached("CanBeDragged",
typeof(bool), typeof(DragBehavior),
new PropertyMetadata(false, OnDragChanged));
public static bool GetCanBeDragged(DependencyObject obj)
{
return (bool)obj.GetValue(IsDragProperty);
}
public static void SetCanBeDragged(DependencyObject obj, bool value)
{
obj.SetValue(IsDragProperty, value);
}
private static void OnDragChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var element = (UIElement)sender;
var isDrag = (bool)(e.NewValue);
dragBehavior = new CanvasDragBehavior();
if (isDrag)
{
element.MouseLeftButtonDown += dragBehavior.ElementOnMouseLeftButtonDown;
element.MouseLeftButtonUp += dragBehavior.ElementOnMouseLeftButtonUp;
element.MouseMove += dragBehavior.ElementOnMouseMove;
}
else
{
element.MouseLeftButtonDown -= dragBehavior.ElementOnMouseLeftButtonDown;
element.MouseLeftButtonUp -= dragBehavior.ElementOnMouseLeftButtonUp;
element.MouseMove -= dragBehavior.ElementOnMouseMove;
}
}
private void ElementOnMouseMove(object sender, MouseEventArgs e)
{
if (!_dragged) return;
Canvas canvas = Extension.FindAncestor<Canvas>(((FrameworkElement)sender));
if (canvas != null)
{
_mouseCurrentPos = e.GetPosition(canvas);
FrameworkElement fe = (FrameworkElement)sender;
if (fe.DataContext.GetType() == typeof(EllipseVM))
{
// EllipseVM class contains X and Y coordinates that are used in ItemsControl to display the ellipse
EllipseVM ellipseVM = (EllipseVM)fe.DataContext;
double positionLeft = _mouseCurrentPos.X - _mouseStartOffset.X;
double positionTop = _mouseCurrentPos.Y - _mouseStartOffset.Y;
#region canvas border check
if (positionLeft < 0) positionLeft = 0;
if (positionTop < 0) positionTop = 0;
if (positionLeft > canvas.ActualWidth) positionLeft = canvas.ActualWidth-fe.Width;
if (positionTop > canvas.ActualHeight) positionTop = canvas.ActualHeight-fe.Height;
#endregion
ellipseVM.left = positionLeft;
ellipseVM.top = positionTop;
}
}
}
private void ElementOnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_mouseStartOffset = e.GetPosition((FrameworkElement)sender);
_dragged = true;
((UIElement)sender).CaptureMouse();
}
private void ElementOnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_dragged = false;
((UIElement)sender).ReleaseMouseCapture();
}
XAML:
<ItemsControl ItemsSource="{Binding scrollViewElements}" >
<ItemsControl.Resources>
<!--some other data templates here-->
</DataTemplate>
<DataTemplate DataType="{x:Type VM:EllipseVM}" >
<Ellipse Width="{Binding width}"
Height="{Binding height}"
Fill="{Binding fillBrush}"
Stroke="Red" StrokeThickness="1"
behaviors:CanvasDragBehavior.CanBeDragged="True"
/>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Transparent" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding top}"/>
<Setter Property="Canvas.Left" Value="{Binding left}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
MVVM distinct 3 kinds of object:
View
VModel
Model
The property of the view should be bound to the VModel, you try correctly to bind the view with EllipseVM, like a real Expert!
The issue on your project is that you have not a single View bound to a single VM, but you want an infinite number of VModels.
I will enumerate below some points of reflection:
I would like to challenge the fact that you register on different drag events:
element.MouseLeftButtonDown
you should register only when objects are created or destroyed.
CanvasDragBehavior : why do you implement a singleton like pattern with a static public property (without private constructor) ?
Avoid registering properties via string like "CanBeDragged" find a way to define and use interfaces eg IMoveFeature{bool IsDraggable}
Your code is too complicated and has some errors.
For example the static instance property of the CanvasDragBehavior is not required. It looks like you confused something here.
To position the element on the Canvas simply use the attached properties Canvas.Top and Canvas.Left.
Prefer the tunneling version of input events, prefixed with Preview. For example listen to PreviewMouseMove instead of MouseMove.
Another important fix is to use the WeakEventManager to subscribe to the events of the attached elements. Otherwise you create a potential memory leak (depending on the lifetime of the event publisher and event listener). Always remember to follow the following pattern to avoid such memory leaks: when you subscribe to events, ensure that you will always unsubscribe too. If you have no control over the object lifetime, always follow the Weak Event pattern and use the WeakEventManager to observe events.
In your case: when an item is removed from the ItemsControl.ItemsSource, your behavior won't be able to detect this change in order to unsubscribe from the corresponding events.
The risk of a memory leak in your context is not high, but better be safe than sorry and stick to the safety pattern.
When implementing a control or behavior, try to avoid tight coupling to data types and implementation details. Make the control or behavior as generic as possible. For this reason, your behavior should not know about the DataContext and what type of elements are dragged. This way you can simply extend your code or reuse the behavior for example to allow to drag a Rectangle too. Right now, your code only works with a Ellipse or EllipseVM.
Usually, you don't need the position data in your view model. If it's pure UI drag&Drop the coordinates are part of the view only. In this case you would prefer to attach the behavior to the item container instead of attaching it to the elements of the DataTemplate: you don't want to drag the data model. You want to drag the item container.
If you still need the coordinates in your model, you would setup a binding in the ItemsControl.ItemContainerStyle like in the example below (the DataContext of the item container Style is always the data item, which is of type EllipseVM in your case).
The simplified and improved version that targets dragging the item container rather than the data model could look as follows. Note that the following behavior is implemented by only using the UIElement type for the dragged object. The actual type of the element or the data model is not required at all. This way it will work with every shape or control (not only Ellipse). You can even drag a Button or whatever is defined in the DataTemplate. The DataContext can be any type.
public class CanvasDragBehavior
{
public static readonly DependencyProperty IsDragEnabledProperty = DependencyProperty.RegisterAttached(
"IsDragEnabled",
typeof(bool),
typeof(CanvasDragBehavior),
new PropertyMetadata(false, OnIsDragEnabledChanged));
public static bool GetIsDragEnabled(DependencyObject obj) => (bool)obj.GetValue(IsDragEnabledProperty);
public static void SetIsDragEnabled(DependencyObject obj, bool value) => obj.SetValue(IsDragEnabledProperty, value);
private static Point DragStartPosition { get; set; }
private static ConditionalWeakTable<UIElement, FrameworkElement> ItemToItemHostMap { get; } = new ConditionalWeakTable<UIElement, FrameworkElement>();
private static void OnIsDragEnabledChanged(object attachingElement, DependencyPropertyChangedEventArgs e)
{
if (attachingElement is not UIElement uiElement)
{
return;
}
var isEnabled = (bool)e.NewValue;
if (isEnabled)
{
WeakEventManager<UIElement, MouseButtonEventArgs>.AddHandler(uiElement, nameof(uiElement.PreviewMouseLeftButtonDown), OnDraggablePreviewMouseLeftButtonDown);
WeakEventManager<UIElement, MouseEventArgs>.AddHandler(uiElement, nameof(uiElement.PreviewMouseMove), OnDraggablePreviewMouseMove);
WeakEventManager<UIElement, MouseButtonEventArgs>.AddHandler(uiElement, nameof(uiElement.PreviewMouseLeftButtonUp), OnDraggablePreviewMouseLeftButtonUp);
}
else
{
WeakEventManager<UIElement, MouseButtonEventArgs>.RemoveHandler(uiElement, nameof(uiElement.PreviewMouseLeftButtonDown), OnDraggablePreviewMouseLeftButtonDown);
WeakEventManager<UIElement, MouseEventArgs>.RemoveHandler(uiElement, nameof(uiElement.PreviewMouseMove), OnDraggablePreviewMouseMove);
WeakEventManager<UIElement, MouseButtonEventArgs>.RemoveHandler(uiElement, nameof(uiElement.PreviewMouseLeftButtonUp), OnDraggablePreviewMouseLeftButtonUp);
}
}
private static void OnDraggablePreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Released)
{
return;
}
var draggable = sender as UIElement;
if (!ItemToItemHostMap.TryGetValue(draggable, out FrameworkElement draggableHost))
{
return;
}
Point newDragEndPosition = e.GetPosition(draggableHost);
newDragEndPosition.Offset(-DragStartPosition.X, -DragStartPosition.Y);
Canvas.SetLeft(draggable, newDragEndPosition.X);
Canvas.SetTop(draggable, newDragEndPosition.Y);
}
private static void OnDraggablePreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var draggable = sender as UIElement;
if (!ItemToItemHostMap.TryGetValue(draggable, out _))
{
if (!TryGetVisualParent(draggable, out Panel draggableHost))
{
return;
}
ItemToItemHostMap.Add(draggable, draggableHost);
}
DragStartPosition = e.GetPosition(draggable);
draggable.CaptureMouse();
}
private static void OnDraggablePreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
=> (sender as UIElement)?.ReleaseMouseCapture();
private static bool TryGetVisualParent<TParent>(DependencyObject element, out TParent parent) where TParent : DependencyObject
{
parent = null;
if (element is null)
{
return false;
}
element = VisualTreeHelper.GetParent(element);
if (element is TParent parentElement)
{
parent = parentElement;
return true;
}
return TryGetVisualParent(element, out parent);
}
}
Usage example
DataItem.cs
class DataItem : INotifyPropertyChanged
{
// Allow this item to change its coordinates aka get dragged
private bool isPositionDynamic;
public bool IsPositionDynamic
{
get => this.isPositionDynamic;
set
{
this.isPositionDynamic = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
MainWindow.xaml
<Window>
<ItemsControl ItemsSource="{Binding DataItems}"
Height="1000"
Width="1000">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:DataItem}">
<Ellipse Width="50"
Height="50"
Fill="Red"
Stroke="Black"
StrokeThickness="1" />
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Width="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl}, Path=ActualHeight}"
Background="Gray" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<!-- If every item is draggable, simply set this property to 'True' -->
<Setter Property="local:CanvasDragBehavior.IsDragEnabled"
Value="{Binding IsPositionDynamic}" />
<!-- Optional binding if you need the coordinates in the view model.
This example assumes that the view model has a Top and Left property -->
<Setter Property="Canvas.Top"
Value="{Binding Top, Mode=TwoWay}" />
<Setter Property="Canvas.Left"
Value="{Binding Left, Mode=TwoWay}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Window>

How to change background of listbox items (Textblock) in UWP

When trying to change background of TextBlock that is part of a DataTemplate of a ListBox the background is only around the text and not the entire block
In UWP TextBlock doesn't have background property so I've wrapped it in a border and changed the border's background like this:
<ListBox x:Name="BitsListView" ItemsSource="{x:Bind BitsList, Mode=TwoWay}" Loaded="BitsListView_Loaded"
HorizontalAlignment="Left" IsEnabled="{x:Bind IsWriteAccess,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
SelectionChanged="BitsListView_SelectionChanged " SelectionMode="Single">
<ListBox.ItemTemplate>
<DataTemplate>
<Border>
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="BitText" Text="{Binding}" Loaded="BitText_Loaded" />
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
And the color is changed in an OnLoaded event like this:
private void BitText_Loaded(object sender, RoutedEventArgs e)
{
TextBlock bitText = sender as TextBlock;
StackPanel sp = bitText.Parent as StackPanel;
Border border = sp.Parent as Border;
if ((int)bitText.DataContext == 1)
{
bitText.Foreground = new SolidColorBrush(Windows.UI.Colors.LightGreen);
border.Background = new SolidColorBrush(Windows.UI.Colors.DarkGreen);
}
else
{
bitText.Foreground = new SolidColorBrush(Windows.UI.Colors.Gray);
border.Background = new SolidColorBrush(Windows.UI.Colors.LightGray);
}
}
But the result is this:
https://pasteboard.co/IlcZB1J.png
What i'm trying to achive is something like this:
(Don't mind the bad MSPaint job)
https://pasteboard.co/Ild1plp.png
What i've tried to do to solve this is wrapping the stackpanel with border, but that didnt help.
then i've tried to wrap the datatemplate but that is not possible, climbing further up the tree changing the backgrounds is not working properly, and obviously changing the ListBox's background paints the entire list, and I need only the blocks that has 1 to be painted fully and not just a little bit around the text
For your question, you do not need to use border to wrap the StackPanel, it will not work. You just need to define a style for ListBoxItem and apply it to ItemContainerStyle and set HorizontalContentAlignment=Stretch.
Here, I checked your code. I have some suggestions for you. In UWP, you could do most things with binding. That means you do not need to find the specific control and set its property value from DataTemplate in the page's code-behind. It's not best practice. Instead, you could define a custom class which includes three properties(text,background,foreground). Then, you could bind to these properties on your XAML page.
The complete code sample like the following:
<ListView x:Name="BitsListView" ItemsSource="{x:Bind BitsList, Mode=TwoWay}"
HorizontalAlignment="Left" SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="VerticalContentAlignment" Value="Stretch"></Setter>
<Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
<Setter Property="Padding" Value="0"></Setter>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" ></StackPanel>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Test">
<StackPanel Background="{x:Bind backGround}">
<TextBlock x:Name="BitText" Text="{x:Bind content}" Foreground="{x:Bind foreGround}" HorizontalTextAlignment="Center"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
public class Test : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _content;
public int content
{
get { return _content; }
set
{
if (_content != value)
{
_content = value;
if (value == 1)
{
foreGround = new SolidColorBrush(Colors.LightGreen);
backGround = new SolidColorBrush(Colors.DarkGreen);
}
else
{
foreGround = new SolidColorBrush(Colors.Gray);
backGround = new SolidColorBrush(Colors.LightGray);
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("content"));
}
}
}
private SolidColorBrush _backGround;
public SolidColorBrush backGround
{
get { return _backGround; }
set
{
if (_backGround != value)
{
_backGround = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("backGround"));
}
}
}
private SolidColorBrush _foreGround;
public SolidColorBrush foreGround
{
get { return _foreGround; }
set
{
if (_foreGround != value)
{
_foreGround = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("foreGround"));
}
}
}
}
public sealed partial class MainPage : Page
{
private ObservableCollection<Test> BitsList { get; set; }
public MainPage()
{
this.InitializeComponent();
BitsList = new ObservableCollection<Test>();
for (int i = 0; i < 10; i++)
{
Random random = new Random();
BitsList.Add(new Test() { content = random.Next(0, 9) });
}
}
}

C# wpf how to draw rectangle on polyline points

I am trying to draw a polyline on a canvas which will have rectangle on each point. The Polyline is bound to a collection of points from the ViewModel.
When I try to set DataTemplate for each point (like below) it shows no rectangle on polyline points.
Is there some way to display rectangle on polyline points?
Later I want to adjust the polyline by dragging these points.
<Polyline Points="{Binding EdgePoints, Converter={StaticResource pointCollectionConverter}}" StrokeThickness="2">
<Polyline.Resources>
<DataTemplate DataType="{x:Type Point}">
<Rectangle Width="20" Height="20" Fill="Black"/>
</DataTemplate>
</Polyline.Resources>
</Polyline>
Here is example where I want to draw rectangles.
You could have a view model like the one shown below. Besides the obvious parts, it attaches/detaches a PropertyChanged handler to/from each Vertex in order to fire the PropertyChanged event for the Vertices property. This is necessary to update the Polyline's Point Binding.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Vertex : ViewModelBase
{
private Point point;
public Point Point
{
get { return point; }
set { point = value; OnPropertyChanged(); }
}
}
public class ViewModel : ViewModelBase
{
public ViewModel()
{
Vertices.CollectionChanged += VerticesCollectionChanged;
}
public ObservableCollection<Vertex> Vertices { get; }
= new ObservableCollection<Vertex>();
private void VerticesCollectionChanged(
object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems.OfType<INotifyPropertyChanged>())
{
item.PropertyChanged += VertexPropertyChanged;
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var item in e.OldItems.OfType<INotifyPropertyChanged>())
{
item.PropertyChanged -= VertexPropertyChanged;
}
}
OnPropertyChanged(nameof(Vertices));
}
private void VertexPropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(nameof(Vertices));
}
}
The Vertices to PointCollection converter could look like this:
public class VerticesConverter : IValueConverter
{
public object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
var vertices = value as IEnumerable<Vertex>;
return vertices != null
? new PointCollection(vertices.Select(v => v.Point))
: null;
}
public object ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
The view would use a Polyline and an ItemsControl. The ItemsTemplate would declare a Thumb element that handles the dragging of vertex points.
<Canvas>
<Canvas.Resources>
<local:VerticesConverter x:Key="VerticesConverter"/>
<Style x:Key="ThumbStyle" TargetType="Thumb">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Rectangle Fill="Transparent" Stroke="Red"
Width="10" Height="10" Margin="-5,-5"/>
</ControlTemplate>
</Setter.Value>
</Setter>
<EventSetter Event="DragDelta" Handler="ThumbDragDelta"/>
</Style>
</Canvas.Resources>
<Polyline Points="{Binding Vertices, Converter={StaticResource VerticesConverter}}"
Stroke="DarkBlue" StrokeThickness="3" StrokeLineJoin="Round"/>
<ItemsControl ItemsSource="{Binding Vertices}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Point.X}"/>
<Setter Property="Canvas.Top" Value="{Binding Point.Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Thumb Style="{StaticResource ThumbStyle}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
Finally, the Thumb's DragDelta handler:
private void ThumbDragDelta(object sender, DragDeltaEventArgs e)
{
var vertex = (Vertex)((Thumb)sender).DataContext;
vertex.Point = new Point(
vertex.Point.X + e.HorizontalChange,
vertex.Point.Y + e.VerticalChange);
}

Bind mouseLeftButtonDown for ListBoxItem and background color on PropertyChanged (mvvm, wpf)

sorry if you think this is a duplication of another post, but I tried every possible solution out there and I can't make it work.
The question is a two parter, but it's about the same code, so I thought I can ask the question in the same thread.
I try to do order system in C#, wpf, vs2015 using mvvm without any (hard coded) couplings. There is two things I need to do. First is to fire an event which I need to capture in the ViewModel, and second, when the number of articles is over a certain level, the background of the listboxitem for that article should be green (otherwise white/grey)
For this I use a ListBox and some listBoxItems.
MainWindow.xaml (the part that matters)
<Window x:Class="Sequence_Application_2.GUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:acb="clr-namespace:AttachedCommandBehavior;assembly=AttachedCommandBehavior"
mc:Ignorable="d"
....
<Window.DataContext>
<Grid Grid.Column="0" Margin="10">
<ListBox x:Name="LstViewFlows" SelectedItem="{Binding SelectedFlow.Name}" ItemsSource="{Binding Flows.Keys}" >
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}" >
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedFlow.CanDeliver, UpdateSourceTrigger=PropertyChanged}" Value="true" >
<Setter Property="ListBoxItem.Background" Value="DarkGreen" />
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<i:InvokeCommandAction Command="{Binding SetSelectedCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
</Grid>
Question 1)
I need to bind a command to a "MouseLeftButtonDown" event when I click on a listboxItem in a listbox. I tried the ACB-solution but can't get it to work. At last I tried the code above and I can't get it to trigger when I click on an item, but below the items, in the empty part of the listbox (not on a listboxitem), it tiggers as it should. Guess I need to rearange my xaml-file or something, but I tried for two days now I nothing seems to work.
The command is named "setSelectedCommand" and is implemented like this
internal class SetSelectedFlowCommand : ICommand
{
private FlowsViewModel _flowsViewModel;
public SetSelectedFlowCommand(FlowsViewModel flowsViewModel)
{
_flowsViewModel = flowsViewModel;
}
/// <summary>
///
/// </summary>
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_flowsViewModel.SetSelectedFlow();
}
}
As I said, if I click on the ListBoxItem - nothing happends, but if i click in the ListBox - the command is fired (but nothing is "selected")
Question 2
As you see in the xaml above, I try to set the background color for a ListBoxitem, based on the value .CanDeliver in an object that is stored in a dictionary in the ViewModel. The variable Flow is the dictionary and SelectedFlow is supposed to be the flow that is selected.
This is an order system and CanDeliver is a variable that tells the operator/user if there is enough products to be delivered to the customer. If there are enough the listboxitem should be green, otherwise remain white/grey. Do you understand my question? Can I do it like this? Refering to an object inside a dictionary? (it's triggered by an INotifyPropertyChanged in the objects)
Hope you can help me because I have no more hair to pull from my head now ;-)
You don't need to use an event handler if all you want is to get the selectedItem. I have build an working example for you to show how to use binding only (MVVM) to achieve your requirements in your question:
C# (ViewModel):
using System;
using System.Collections.Generic;
using System.Windows;
using System.ComponentModel;
using System.Collections.ObjectModel;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MyViewModel mvm = new MyViewModel()
{
Flows = new ObservableCollection<Flow>()
{
new Flow() { Name = "Flow1" },
new Flow() { Name = "Flow2" },
new Flow() { Name = "Flow3" , Amount=1},
new Flow() { Name = "Flow4" }
}
};
this.DataContext = mvm;
}
}
public class MyViewModel : ObservableObject
{
private Flow _selectedflow;
public ObservableCollection<Flow> Flows
{
get;
set;
}
public Flow SelectedFlow
{
get { return _selectedflow; }
set
{
if (value != _selectedflow)
{
_selectedflow = value;
RaisePropertyChanged("SelectedFlow");
}
}
}
}
public class Flow : ObservableObject
{
private string _name;
private int _amount;
public string Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
RaisePropertyChanged("Name");
}
}
}
public bool CanDeliver
{
get
{
return Amount > 0;
}
}
public int Amount
{
get { return _amount; }
set
{
if (value != _amount)
{
_amount = value;
RaisePropertyChanged("Amount");
RaisePropertyChanged("CanDeliver");
}
}
}
}
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
protected void RaisePropertyChanged(String propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
}
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="350">
<Grid>
<StackPanel Orientation="Vertical">
<ListBox SelectedItem="{Binding SelectedFlow}" ItemsSource="{Binding Flows}" DisplayMemberPath="Name">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}" >
<Style.Triggers>
<DataTrigger Binding="{Binding CanDeliver, UpdateSourceTrigger=PropertyChanged}" Value="true" >
<Setter Property="Background" Value="DarkGreen" />
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<TextBlock Text="{Binding SelectedFlow.Name}"/>
</StackPanel>
</Grid>
</Window>
Result: You can see the Flow item with CanDeliver = True (Amount>0) has a Green background. And the TextBlock is showing the SelectedFlow.

C# Windows Forms graphical component for Lee Algorithm

I have a task of programming a simple program-demonstration of the Lee Algorithm for pathfinding in a maze. I want to make it somewhat graphically interactive: create a 2D table with a variable amount of rows and columns, which's cells can be clicked (and the position of the clicked cell should be able to be tracked). This is because I want to let the user draw the maze obstacles, set the start point etc. What would be the best graphical component that could help me do this and how can I interact with its' cells?
Following on from your comment I would say that WPF is a more natural candidate for this task because it has been designed to do custom layout stuff. Here is a code example I've put together using an items control with a uniform grid that displays a grid of cells - clicking on a cell selects it, clicking again deselects it.
This isn't great WPF, but it might give you some ideas and get you started.
Application:
XAML:
<Window x:Class="LeeAlgorithm.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="380" Width="350">
<Grid>
<ItemsControl ItemsSource="{Binding Cells}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="18"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</DataTemplate.Resources>
<Grid Width="30" Height="30" Margin="2">
<DockPanel ZIndex="9999">
<!-- This is a hack to capture the mouse click in the area of the item -->
<Rectangle
Fill="Transparent"
DockPanel.Dock="Top"
MouseDown="UIElement_OnMouseDown"
Tag="{Binding}" />
</DockPanel>
<Grid>
<Rectangle Fill="{Binding Path=Color}" Stroke="Red" StrokeDashArray="1 2" />
<TextBlock Margin="3,3,3,0" Text="{Binding Path=CellNumber}"/>
</Grid>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Code Behind (your MainWindow.xaml.cs code):
namespace LeeAlgorithm
{
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
this.Cells = new ObservableCollection<Cell>();
for (int i = 0; i != 50; ++i)
{
this.Cells.Add(new Cell { CellNumber = i });
}
InitializeComponent();
DataContext = this;
}
public ObservableCollection<Cell> Cells { get; private set; }
private void UIElement_OnMouseDown(object sender, MouseButtonEventArgs e)
{
var cell = (Cell)((Rectangle)sender).Tag;
if (!cell.IsSelected)
{
cell.Color = new SolidColorBrush(Colors.HotPink);
cell.IsSelected = true;
}
else
{
cell.Color = new SolidColorBrush(Colors.Silver);
cell.IsSelected = false;
}
}
}
public class Cell : INotifyPropertyChanged
{
private int cellNumber;
private SolidColorBrush color = new SolidColorBrush(Colors.Silver);
public event PropertyChangedEventHandler PropertyChanged;
public int CellNumber
{
get
{
return this.cellNumber;
}
set
{
if (value == this.cellNumber)
{
return;
}
this.cellNumber = value;
this.OnPropertyChanged("CellNumber");
}
}
public SolidColorBrush Color
{
get
{
return this.color;
}
set
{
if (Equals(value, this.color))
{
return;
}
this.color = value;
this.OnPropertyChanged("Color");
}
}
public bool IsSelected { get; set; }
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Happy coding!

Categories

Resources