WPF 4 Drag and Drop with visual element as cursor - wpf

I have a WPF 4 app which I want to enable drag and drop with, currently I have it working with a basic drag and drop implementation, but I have found that it would be much better if, instead of the mouse cursor changing over to represent the move operation, I could use an image underneath my finger.
My drag and drop operation is initiated inside a custom user control, so I will need to insert a visual element into the visual tree and have it follow my finger around, perhaps I should enable the ManipulationDelta event on my main window, check for a boolean then move the item around?

From the mentioned article I was able to simplify a little. Basically what you need to do is subscribe in 3 events:
PreviewMouseLeftButtonDownEvent: The event that runs when you press the left button, you can start the drag action by invoking DragDrop.DoDragDrop
DropEvent: The event that runs when you drop something (control must have AllowDrop set to true in order to accept drops)
GiveFeedbackEvent: The event that runs all the time allowing you to give constant feedback
DragDrop.DoDragDrop(draggedItem, draggedItem.DataContext, DragDropEffects.Move); the first parameter is the element you are dragging, then the second is the data it is carrying and last the mouse effect.
This method locks the thread. So everything after its call will only execute when you stop dragging.
In the drop event you can retrieve the data you sent on the DoDragDrop call.
The source for my tests are located bellow, and the result is:
Sample drag n' drop (gif)
Full Source
MainWindow.xaml
<Window x:Class="TestWpfPure.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uc="clr-namespace:TestWpfPure"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListBox x:Name="CardListControl" AllowDrop="True" ItemsSource="{Binding Items}" />
</Grid>
</Window>
Card.xaml
<UserControl x:Class="TestWpfPure.Card"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Border x:Name="CardBorder" BorderBrush="Black" BorderThickness="3" HorizontalAlignment="Left" Height="40" VerticalAlignment="Top" Width="246" RenderTransformOrigin="0.5,0.5" CornerRadius="6">
<TextBlock Text="{Binding Text}" TextWrapping="Wrap" FontFamily="Arial" FontSize="14" />
</Border>
</Grid>
</UserControl>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Shapes;
namespace TestWpfPure
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ObservableCollection<Card> Items { get; set; }
private readonly Style listStyle = null;
private Window _dragdropWindow = null;
public MainWindow()
{
InitializeComponent();
Items = new ObservableCollection<Card>(new List<Card>
{
new Card { Text = "Task #01" },
new Card { Text = "Task #02" },
new Card { Text = "Task #03" },
new Card { Text = "Task #04" },
new Card { Text = "Task #05" },
});
listStyle = new Style(typeof(ListBoxItem));
listStyle.Setters.Add(new Setter(ListBoxItem.AllowDropProperty, true));
listStyle.Setters.Add(new EventSetter(ListBoxItem.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(CardList_PreviewMouseLeftButtonDown)));
listStyle.Setters.Add(new EventSetter(ListBoxItem.DropEvent, new DragEventHandler(CardList_Drop)));
listStyle.Setters.Add(new EventSetter(ListBoxItem.GiveFeedbackEvent, new GiveFeedbackEventHandler(CardList_GiveFeedback)));
CardListControl.ItemContainerStyle = listStyle;
DataContext = this;
}
protected void CardList_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender is ListBoxItem)
{
var draggedItem = sender as ListBoxItem;
var card = draggedItem.DataContext as Card;
card.Effect = new DropShadowEffect
{
Color = new Color { A = 50, R = 0, G = 0, B = 0 },
Direction = 320,
ShadowDepth = 0,
Opacity = .75,
};
card.RenderTransform = new RotateTransform(2.0, 300, 200);
draggedItem.IsSelected = true;
// create the visual feedback drag and drop item
CreateDragDropWindow(card);
DragDrop.DoDragDrop(draggedItem, draggedItem.DataContext, DragDropEffects.Move);
}
}
protected void CardList_Drop(object sender, DragEventArgs e)
{
var droppedData = e.Data.GetData(typeof(Card)) as Card;
var target = (sender as ListBoxItem).DataContext as Card;
int targetIndex = CardListControl.Items.IndexOf(target);
droppedData.Effect = null;
droppedData.RenderTransform = null;
Items.Remove(droppedData);
Items.Insert(targetIndex, droppedData);
// remove the visual feedback drag and drop item
if (this._dragdropWindow != null)
{
this._dragdropWindow.Close();
this._dragdropWindow = null;
}
}
private void CardList_GiveFeedback(object sender, GiveFeedbackEventArgs e)
{
// update the position of the visual feedback item
Win32Point w32Mouse = new Win32Point();
GetCursorPos(ref w32Mouse);
this._dragdropWindow.Left = w32Mouse.X;
this._dragdropWindow.Top = w32Mouse.Y;
}
private void CreateDragDropWindow(Visual dragElement)
{
this._dragdropWindow = new Window();
_dragdropWindow.WindowStyle = WindowStyle.None;
_dragdropWindow.AllowsTransparency = true;
_dragdropWindow.AllowDrop = false;
_dragdropWindow.Background = null;
_dragdropWindow.IsHitTestVisible = false;
_dragdropWindow.SizeToContent = SizeToContent.WidthAndHeight;
_dragdropWindow.Topmost = true;
_dragdropWindow.ShowInTaskbar = false;
Rectangle r = new Rectangle();
r.Width = ((FrameworkElement)dragElement).ActualWidth;
r.Height = ((FrameworkElement)dragElement).ActualHeight;
r.Fill = new VisualBrush(dragElement);
this._dragdropWindow.Content = r;
Win32Point w32Mouse = new Win32Point();
GetCursorPos(ref w32Mouse);
this._dragdropWindow.Left = w32Mouse.X;
this._dragdropWindow.Top = w32Mouse.Y;
this._dragdropWindow.Show();
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetCursorPos(ref Win32Point pt);
[StructLayout(LayoutKind.Sequential)]
internal struct Win32Point
{
public Int32 X;
public Int32 Y;
};
}
}
Card.xaml.cs
using System.ComponentModel;
using System.Windows.Controls;
namespace TestWpfPure
{
/// <summary>
/// Interaction logic for Card.xaml
/// </summary>
public partial class Card : UserControl, INotifyPropertyChanged
{
private string text;
public string Text
{
get
{
return this.text;
}
set
{
this.text = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Text"));
}
}
public Card()
{
InitializeComponent();
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
}
}

There is an example of using a custom drag cursor at Jaime Rodriguez msdn blog. You can handle the GiveFeedback event and change the mouse cursor, but to use a custom Visual the author creates a new Window and updates the position on QueryContinueDrag.

Related

wpf Button always disabled (with CommandBinding, CanExecute=True and IsEnabled= True)

Revised: I apologize for missing some important descriptions in the first version, now the problem should be well-defined:
so I'm making a toy CAD program with following views:
MainWindow.xaml
CustomizedUserControl.xaml
CustomizedUserControl is a Tab within MainWindow, and its DataContext is defined in MainWindow.xaml as:
<Window.Resources>
<DataTemplate DataType="{x:Type local:CustomizedTabClass}">
<local:UserControl1/>
</DataTemplate>
</Window.Resources>
And CustomizedUserControl.xaml provides a canvas and a button, so when the button is pressed the user should be able to draw on the canvas. As the following code shows, the content of Canvas is prepared by the dataContext, "tabs:CustomizedTabClass".
CustomizedUserControl.xaml
<CustomizedUserControl x:Name="Views.CustomizedUserControl11"
...
>
<Button ToolTip="Lines (L)" BorderThickness="2"
Command="{Binding ElementName=CustomizedUserControl11,
Path=DrawingCommands.LinesChainCommand}"
IsEnabled="True"
Content = "{Binding ElementName=CustomizedUserControl11,
Path=DrawingCommands.Button1Name}">
</Button>
...
<canvas x:Name="CADCanvas"
Drawing="{Binding Drawing ,Mode=TwoWay}" >
</canvas>
It is also notable that I used an external library, Fody/PropertyChanged, in all classes so property notifications would be injected without further programming.
CustomizedUserControl.xaml.cs
using PropertyChanged;
using System.ComponentModel;
using System.Windows.Controls;
[AddINotifyPropertyChangedInterface]
public partial class CustomizedUserControl: Usercontrol, INotifyPropertyChanged{
public CADDrawingCommands DrawingCommands { get; set; }
public CustomizedUserControl()
{
InitializeComponent();
DrawingCommands = new CADDrawingCommands(this);
DrawingCommands.Button1Name = "yeahjojo"; //For testing data binding
}
public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };
}
CADDrawingCommands.cs
using PropertyChanged;
using System.ComponentModel;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows;
[AddINotifyPropertyChangedInterface]
public class CADDrawingCommands : INotifyPropertyChanged{
UserControl _drawableTab;
public string Button1Name { get; set; } = "TestForDataBinding";
public RoutedCommand LinesChainCommand { get; set; } = new RoutedCommand();
public CADDrawingCommands(UserControl dTab){
_drawableTab = dTab;
CommandBinding lineCommandBinding = new CommandBinding(LinesChainCommand,
(object sender, ExecutedRoutedEventArgs e) =>
{
MessageBox.Show("Test");
//Draw on canvas inside CustomizedUserControl (modify Drawing property in CustomizedTabClass)
}, (object sender, CanExecuteRoutedEventArgs e) => { e.CanExecute = true; });
_drawableTab.CommandBindings.Add(lineCommandBinding);
}
public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };
}
The Content of Button is set correctly, as I can read the string defined in Button1Name:
Therefore I suppose the Data Binding for Command is also ok. IsEnabled has been set to true and CanExecute of the CommandBinding would only return true.
Why is my button still greyed out and not clickable?
If I define the button inside a Window instead of UserControl (and set the datacontext of the Window to its own code behind, the button will be clickable! Why?
Thank you for your time! Hopefully would somebody help me cuz I've run out of ideas and references.
Made the simplest example.
Everything works as it should.
BaseInpc is my simple INotifyPropertyChanged implementation from here: BaseInpc
using Simplified;
using System.Windows;
using System.Windows.Input;
namespace CustomizedUserControlRoutedCommand
{
public class CADDrawingCommands : BaseInpc
{
UIElement _drawableTab;
private string _button1Name = "TestForDataBinding";
public string Button1Name { get => _button1Name; set => Set(ref _button1Name, value); }
public static RoutedCommand LinesChainCommand { get; } = new RoutedCommand();
public CADDrawingCommands(UIElement dTab)
{
_drawableTab = dTab;
CommandBinding lineCommandBinding = new CommandBinding(LinesChainCommand,
(object sender, ExecutedRoutedEventArgs e) =>
{
MessageBox.Show("Test");
//Draw on canvas inside CustomizedUserControl (modify Drawing property in CustomizedTabClass)
}, (object sender, CanExecuteRoutedEventArgs e) => { e.CanExecute = true; });
_drawableTab.CommandBindings.Add(lineCommandBinding);
}
}
}
<UserControl x:Name="CustomizedUserControl11" x:Class="CustomizedUserControlRoutedCommand.CustomizedUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:CustomizedUserControlRoutedCommand"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Button ToolTip="Lines (L)" BorderThickness="2"
Command="{x:Static local:CADDrawingCommands.LinesChainCommand}"
IsEnabled="True"
Content = "{Binding ElementName=CustomizedUserControl11,
Path=DrawingCommands.Button1Name}">
</Button>
</Grid>
</UserControl>
using System.Windows.Controls;
namespace CustomizedUserControlRoutedCommand
{
public partial class CustomizedUserControl : UserControl
{
public CADDrawingCommands DrawingCommands { get; }
public CustomizedUserControl()
{
DrawingCommands = new CADDrawingCommands(this);
InitializeComponent();
DrawingCommands.Button1Name = "yeahjojo"; //For testing data binding
}
}
}
<Window x:Class="CustomizedUserControlRoutedCommand.TestCustomizedUserControlWindow"
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:local="clr-namespace:CustomizedUserControlRoutedCommand"
mc:Ignorable="d"
Title="TestCustomizedUserControlWindow" Height="450" Width="800">
<Grid>
<local:CustomizedUserControl/>
</Grid>
</Window>
If you showed your code in full, then I see the following problems in it:
You are setting the value incorrectly for the DrawingCommands property.
In this property, you do not raise PropertyChanged.
The binding in the Button is initialized in the InitializeComponent() method. At this point, the property is empty, and when you set a value to it, the binding cannot find out.
There are two ways to fix this:
Raise PropertyChanged in the property;
If you set the property value once in the constructor, then set it immediately in the initializer. Make the property "Read Only". This way, in my opinion, is better.
public CADDrawingCommands DrawingCommands { get; }
public FileEditTabUserControl()
{
DrawingCommands = new CADDrawingCommands(this);
InitializeComponent();
DrawingCommands.Button1Name = "yeahjojo"; //For testing data binding
}
You have a button bound to a command in the DrawingCommands.LinesChainCommand property.
But to this property, you assign an empty instance of the = new RoutedCommand () routing command.
This looks pointless enough.
If you need a routable command, create it in the "Read Only" static property.
This will make it much easier to use in XAML:
public static RoutedCommand LinesChainCommand { get; } = new RoutedCommand();
<Button ToolTip="Lines (L)" BorderThickness="2"
Command="{x:Static local:DrawingCommands.LinesChainCommand}"
IsEnabled="True"
Content = "{Binding ElementName=CustomizedUserControl11,
Path=DrawingCommands.Button1Name}">
</Button>
Raising PropertyChanged in CADDrawingCommands properties is also not visible in your code.
If it really does not exist, then the binding is also unaware of changing property values.

WPF issue with a resizable / collapsible usercontrol

I'm trying to create a usercontrol that can resize and collapse/expand (with an animation). The resizing stop working when I play the collapse/expand animation.
Complete test application can be found here: App
EDIT: here's the relevant code as requested
MyControl.xaml:
<UserControl x:Class="WpfApp1.MyControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300">
<Grid Background="#FF935E5E">
<Thumb Width="8"
HorizontalAlignment="Right"
Margin="0,0,-4,0"
DragDelta="Thumb_DragDelta"
Cursor="SizeWE"/>
</Grid>
MyControl.xaml.cs:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MyControl.xaml
/// </summary>
public partial class MyControl : UserControl
{
public bool IsOpen
{
get { return (bool)GetValue(IsOpenProperty); }
set { SetValue(IsOpenProperty, value); }
}
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register("IsOpen", typeof(bool), typeof(MyControl), new PropertyMetadata(true, OnIsOpenChanged));
private static void OnIsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MyControl control = d as MyControl;
control.PlayAnimation();
}
public double OpenWidth
{
get { return (double)GetValue(OpenWidthProperty); }
set { SetValue(OpenWidthProperty, value); }
}
public static readonly DependencyProperty OpenWidthProperty =
DependencyProperty.Register("OpenWidth", typeof(double), typeof(MyControl), new PropertyMetadata(300d, OnOpenWidthChanged));
private static void OnOpenWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MyControl control = d as MyControl;
if (control.IsOpen)
control.Width = control.OpenWidth;
}
public MyControl()
{
InitializeComponent();
if (IsOpen)
Width = OpenWidth;
}
private void Thumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
OpenWidth += e.HorizontalChange;
}
private void PlayAnimation()
{
DoubleAnimation sizeAnimation = new DoubleAnimation(IsOpen ? OpenWidth : 0, TimeSpan.FromMilliseconds(250));
sizeAnimation.EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseInOut };
BeginAnimation(WidthProperty, sizeAnimation);
}
}
}
MainWindow.xaml:
<Window x:Class="WpfApp1.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:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="700">
<DockPanel>
<local:MyControl IsOpen="{Binding ControlIsOpen}"
OpenWidth="{Binding ControlOpenWidth}"/>
<Grid Background="Green">
<Button Width="100"
Height="20"
Content="Test Animation"
Click="Button_Click"/>
</Grid>
</DockPanel>
MainWindow.xaml.cs:
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
#endregion INotifyPropertyChanged
private bool _ControlIsOpen = true;
public bool ControlIsOpen
{
get => _ControlIsOpen;
set
{
_ControlIsOpen = value;
OnPropertyChanged();
}
}
private double _ControlOpenWidth = 300d;
public double ControlOpenWidth
{
get => _ControlOpenWidth;
set
{
_ControlOpenWidth = value;
OnPropertyChanged();
}
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ControlIsOpen = !ControlIsOpen;
}
}
}
Thanks for the help :)
The animation actually never stopsYou should specifiy FillBehavior to Stop. In this case the annimation will stop updating the property after the final value is reached.
private void PlayAnimation()
{
DoubleAnimation sizeAnimation = new DoubleAnimation(IsOpen ? OpenWidth : 0, TimeSpan.FromMilliseconds(250));
sizeAnimation.FillBehavior = FillBehavior.Stop;
sizeAnimation.EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseInOut };
sizeAnimation.Completed += OnAnimationCompleted;
BeginAnimation(WidthProperty, sizeAnimation);
}
private void OnAnimationCompleted(object sender, EventArgs e)
{
Width = IsOpen ? OpenWidth : 0;
}
The default value is HoldEnd. And the storyboard will modify the Width untill it is not explicitly stopped.
https://msdn.microsoft.com/en-us/library/system.windows.media.animation.timeline.fillbehavior(v=vs.110).aspx
Some more info https://learn.microsoft.com/en-us/dotnet/framework/wpf/graphics-multimedia/how-to-set-a-property-after-animating-it-with-a-storyboard
Well thanks to Dmitry idea, i've been able to solve it, by setting the fill behavior to stop and forcing to width to be either 0 or the open width:
private void PlayAnimation()
{
DoubleAnimation sizeAnimation = new DoubleAnimation(IsOpen ? OpenWidth : 0, TimeSpan.FromMilliseconds(250));
sizeAnimation.EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseInOut };
sizeAnimation.FillBehavior = FillBehavior.Stop;
sizeAnimation.Completed += (s, e) => Width = (IsOpen ? OpenWidth : 0);
BeginAnimation(WidthProperty, sizeAnimation);
}
Thanks all :)

OxyPlot WPF not working with Button Click

I’m having some problems with OxyPlot that I have not been able to resolve through their documentation or other searches. I’m working on a wpf application that will allow the user to open a .csv with a button-click event, then perform some math and report back some useful information. I’d like to plot some of the generated data hence OxyPlot. For some reason I cannot get the plot to populate, when the code that generates it, is within the button click event. To illustrate here is a smaller example:
This code works (xaml):
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:oxy="http://oxyplot.org/wpf"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button x:Name="button" Content="Button" HorizontalAlignment="Left" Margin="20,20,0,0" VerticalAlignment="Top" Width="75" Click="button_Click"/>
<Grid HorizontalAlignment="Left" Height="255" Margin="20,47,0,0" VerticalAlignment="Top" Width="477">
<oxy:PlotView Model="{Binding ScatterModel}"/>
</Grid>
</Grid>
with this:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
var tmp = new PlotModel { Title = "Scatter plot", Subtitle = "y = x" };
var s2 = new LineSeries
{
StrokeThickness = 1,
MarkerSize = 1,
MarkerStroke = OxyColors.ForestGreen,
MarkerType = MarkerType.Plus
};
for (int i = 0; i < 100; i++)
{
s2.Points.Add(new DataPoint(i, i));
}
tmp.Series.Add(s2);
this.ScatterModel = tmp;
}
private void button_Click(object sender, RoutedEventArgs e)
{
}
public PlotModel ScatterModel { get; set; }
And produces this:
Plot Working
But, without changing the xaml, if I copy/paste the code beneath the button click event:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void button_Click(object sender, RoutedEventArgs e)
{
DataContext = this;
var tmp = new PlotModel { Title = "Scatter plot", Subtitle = "y = x" };
var s2 = new LineSeries
{
StrokeThickness = 1,
MarkerSize = 1,
MarkerStroke = OxyColors.ForestGreen,
MarkerType = MarkerType.Plus
};
for (int i = 0; i < 100; i++)
{
s2.Points.Add(new DataPoint(i, i));
}
tmp.Series.Add(s2);
this.ScatterModel = tmp;
}
public PlotModel ScatterModel { get; set; }
The plot never generates: Not working:
I’ve tried moving DataContext = this; back up to public MainWindow(), and vice-versa with InitializeComponent(); no change. I’ve also tried defining
<Window.DataContext>
<local:MainWindow/>
</Window.DataContext>
in the xaml but that throws an exception/infinite loop error during build.
Something simple I fear I'm not getting about OxyPlot implementation?
Thanks!
CSMDakota
INotifyPropertyChanged keeps your view in sync with the program's state. One way to do this is by implementing a ViewModel (the MVVM pattern).
So let's create one. ViewModelBase introduces OnPropertyChanged(), the method that updates ScatterModel.
ViewModels.cs
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using OxyPlot;
namespace WpfApplication1
{
public class ViewModel : ViewModelBase
{
private PlotModel _scatterModel;
public PlotModel ScatterModel
{
get { return _scatterModel; }
set
{
if (value != _scatterModel)
{
_scatterModel = value;
OnPropertyChanged();
}
}
}
}
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] String propName = null)
{
// C#6.O
// PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
}
In MainWindow.xaml you can now add
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void button_Click(object sender, RoutedEventArgs e)
{
var tmp = new PlotModel { Title = "Scatter plot", Subtitle = "y = x" };
var s2 = new LineSeries
{
StrokeThickness = 1,
MarkerSize = 1,
MarkerStroke = OxyColors.ForestGreen,
MarkerType = MarkerType.Plus
};
for (int i = 0; i < 100; i++)
{
s2.Points.Add(new DataPoint(i, i));
}
tmp.Series.Add(s2);
ViewModel.ScatterModel = tmp;
}
// C#6.O
// public ViewModel ViewModel => (ViewModel)DataContext;
public ViewModel ViewModel
{
get { return (ViewModel)DataContext; }
}
}
Note we're no longer setting DataContext = this, which is considered bad practice. In this case the ViewModel is small, but as a program grows this way of structuring pays off.

BitmapSource binding memory leak

The problem is the same with BitmapImage but for demonstration let's use BitmapSource. Basically if I'm showing a fairly large image in a view and I set the bound object to null when I'm done with it the image memory does not get reclaimed no matter how long you wait. The full code to reproduce the problem:
xaml:
<Window x:Class="TestBitmapSource.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:local="clr-namespace:TestBitmapSource"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<Button Command="{Binding Path=ReloadCommand}">Load</Button>
<Button Command="{Binding Path=ReleaseCommand}">Release</Button>
<Image Source="{Binding Path=MyImage}"></Image>
</StackPanel>
</Grid>
C#:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace TestBitmapSource
{
class MainViewModel : INotifyPropertyChanged
{
private BitmapSource myImage;
public BitmapSource MyImage
{
get { return myImage; }
set { myImage = value; NotifyPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private ICommand _reloadCommand;
public ICommand ReloadCommand
{
get
{
return _reloadCommand ?? (_reloadCommand = new CommandHandler(() => Reload(), true));
}
}
private ICommand _releaseCommand;
public ICommand ReleaseCommand
{
get
{
return _releaseCommand ?? (_releaseCommand = new CommandHandler(() => Release(), true));
}
}
Random random = new Random();
public void Reload()
{
int width = 6000;
int height = 6000;
int b = random.Next();
int g = random.Next();
int r = random.Next();
unsafe
{
byte* pixels = (byte*)Marshal.AllocHGlobal(width * height * 3);
for (int i = 0; i < width * height; i++)
{
pixels[i * 3] = (byte)b;
pixels[i * 3 + 1] = (byte)g;
pixels[i * 3 + 2] = (byte)r;
}
MyImage = BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgr24, null, (IntPtr)pixels, width * height * 3, width * 3);
Marshal.FreeHGlobal((IntPtr)pixels);
MyImage.Freeze();
}
}
public void Release()
{
MyImage = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
public MainViewModel()
{
MyImage = null;
}
}
public class CommandHandler : ICommand
{
private Action _action; private bool _canExecute;
public CommandHandler(Action action, bool canExecute) { _action = action; _canExecute = canExecute; }
public bool CanExecute(object parameter) { return _canExecute; }
public event EventHandler CanExecuteChanged;
public void Execute(object parameter) { _action(); }
}
}
To reproduce the problem, click the Load button (which will create the image with BitmapSource.Create) and then the Release button which attempts to release the image and collect unused memory. However, the process monitor shows that the 250MB of memory is still in use, and I know that the MyImage object is still alive if I tag it with an Object ID in Visual Studio.
Two things I've found so far:
1) If I don't bind to the image in the view, everything is fine and the image object disappears immediately.
2) If you click the Release button a second time, the tagged object suddenly disappears and the memory is reclaimed.
So clearly, the act of binding creates some reference somewhere that is not released when the image object is set to null in the view model. I wanted to check if I'm missing something obvious before I get into a memory profiler.
Thanks

wpf databound custom control - why are changes to my datasource not detected by the custom control?

I have a custom control and a view model object. A property on the view model is bound to the custom control and I can see that the custom control actually receives the vaule from the view model object - yet my handler code (GeometryText.Set) is not executed. What am I doing wrong?!
Notice the event handlers on custom control where I've placed breakpoints- if I change the size of the window, I can inspect the GeometryText property in the watch window - and it's clearly updated in the cases where I expect it to.
Thanks for any input,
Anders, Denmark
ComponentDrawing.xaml.cs
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using Rap1D.ServiceLayer.Interfaces.Services;
using StructureMap;
namespace Rap1D.Rap1D_WPF.Controls
{
/// <summary>
/// Interaction logic for ComponentDrawing.xaml
/// </summary>
public partial class ComponentDrawing
{
public static DependencyProperty GeometryTextProperty =DependencyProperty.Register("GeometryText", typeof (string), typeof (ComponentDrawing), new FrameworkPropertyMetadata
(
"",
FrameworkPropertyMetadataOptions
.
None));
private Canvas _canvas;
public ComponentDrawing()
{
InitializeComponent();
}
public string GeometryText
{
get { return ((string) GetValue(GeometryTextProperty)); }
set
{
SetValue(GeometryTextProperty, value);
ReadGeometryTextIntoDrawing(value);
}
}
private void ReadGeometryTextIntoDrawing(string fileText)
{
// Allow control to be visible at design time without errors shown.
// I.e. - don't execute code below at design time.
if (DesignerProperties.GetIsInDesignMode(this))
return;
// OK - we are a running application
//if (_canvas != null)
// return;
// OK - this is first time (-ish) we are running
if (ActualWidth == 0)
return;
// We have a valid screen to pain on
var componentDrawingService = ObjectFactory.GetInstance<IComponentDrawingService>();
//var commandTextProvider = ObjectFactory.GetInstance<ICommandTextProvider>();
//var fileText = ((IViewModelBase) DataContext).GeometryText;
// If getting the file text fails for some reason, just abort to avoid further problems.
if (fileText == null)
return;
var pg = componentDrawingService.GetDrawings(fileText, 0, ActualWidth, 0, ActualHeight);
_canvas = new Canvas();
foreach (var path in pg)
{
_canvas.Children.Add(path);
}
Content = _canvas;
}
private void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
//ReadGeometryTextIntoDrawing();
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
//ReadGeometryTextIntoDrawing();
}
private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e)
{
//ReadGeometryTextIntoDrawing();
}
}
}
ComponentDrawing.xaml:
<UserControl x:Class="Rap1D.Rap1D_WPF.Controls.ComponentDrawing" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"
DataContextChanged="UserControl_DataContextChanged" Loaded="UserControl_Loaded" SizeChanged="UserControl_SizeChanged">
<Grid Background="White">
<Path Stroke="Black"></Path>
</Grid>
</UserControl>
Usage:
<Controls:RadPane x:Class="Rap1D.Rap1D_WPF.Controls.ProductComponentDetails" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:Controls="clr-namespace:Telerik.Windows.Controls;assembly=Telerik.Windows.Controls.Docking" xmlns:Controls1="clr-namespace:Rap1D.Rap1D_WPF.Controls" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" Header="{Binding DisplayName}">
<Controls1:ComponentDrawing GeometryText="{Binding GeometryText}" />
</Controls:RadPane>
view model object (implementing INotifyPropertyChanged):
using System;
using Microsoft.Practices.Prism.Events;
using Rap1D.ExternalInterfaceWrappers.Interfaces;
using Rap1D.ModelLayer.Interfaces.Adapters;
using Rap1D.ModelLayer.Interfaces.Structure;
using Rap1D.ServiceLayer.Interfaces.Adapters;
using Rap1D.ServiceLayer.Interfaces.Providers;
using Rap1D.ViewModelLayer.Interfaces;
namespace Rap1D.ViewModelLayer.Implementations
{
public class ProductComponentViewModel : TreeViewItemViewModel, IProductComponentViewModel
{
...
public override string GeometryText
{
get
{
var pentaResponse = _commandTextProvider.GetCommandText(ProductComponent);
return DateTime.Now.ToString()+ pentaResponse.Payload;
}
}
...
}
}
Dependency property setters are not invoked if changed by binding. If you want to somehow react on dependency property value changing you should register a callback in property metadata:
http://msdn.microsoft.com/en-us/library/ms557294.aspx
something like that (not sure it is compilable, let me know if something wrong):
public static DependencyProperty GeometryTextProperty =
DependencyProperty.Register(... , new FrameworkPropertyMetadata(GeometryTextCallback));
public static void GeometryTextCallback(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
// cast source to your type and invoke method from your setter
((ComponentDrawing)source)ReadGeometryTextIntoDrawing(value);
}

Resources