WPF Drop event is firing twice - wpf

2nd Edit
so removing the Panel.ZIndex properties from the control template resolved this issue for me, giving me 1 drop event.
including them triggers two drop events.
can any one answer me why though?
id love to know why z index ?
Original Question :
I am trying to add a custom object (state) to a canvas called MainCanvas on the MainWindow.
I am trying to drag a state object from a wrap panel and drop it onto the canvas.
the code works but there are two items being added.
I know there are two because I can move the two item around the canvas.
I have searched existing answers and added e.Handled=true, but still adds two items
I tried using Drop event and PreviewDrop Event on MainCanvas, no difference.
Cam someone help as to how I can make it so that only 1 item gets added?
the maincanvas exists at design time
a new state is created at runtime at the drop event.
Here is the OnMouseMove handler for the state
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed)
{
var parent = VisualTreeHelper.GetParent(this);
if (parent as WrapPanel != null)
{
DataObject dragData = new DataObject();
dragData.SetData(DataFormats.StringFormat, this.ItemType);
DragDrop.DoDragDrop(this, dragData, DragDropEffects.Copy);
}
}
e.Handled = true;
}
Within the Code Behind I have set the following events for the canvas:
private void MainCanvas_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.Text))
e.Effects = DragDropEffects.Copy;
else
e.Effects = DragDropEffects.None;
e.Handled = true;
}
private void MainCanvas_Drop(object sender, DragEventArgs e)
{
var itemType = e.Data.GetData(typeof(string));
switch (itemType)
{
case "state":
var pos = e.GetPosition(this.MainCanvas);
State item = new State();
item.Template = (ControlTemplate)FindResource("StateViewModelControlTemplate");
this.MainCanvas.Children.Add(item);
Canvas.SetLeft(item, pos.X);
Canvas.SetTop(item, pos.Y);
e.Handled = true;
break;
default:
break;
}
e.Handled = true;
}
Finally here is the xaml for the Main Canvas
<Canvas x:Name="MainCanvas" x:Name="MainCanvas"
DockPanel.Dock="Top"
Background="#666"
Height="600"
Margin="4"
AllowDrop="True"
DragEnter="MainCanvas_DragEnter"
Drop="MainCanvas_Drop"/>
Edit:
ok so after lupus' response i went back and reconstructed everything from scratch in a separate temp project
<ControlTemplate x:Key="StateViewModelControlTemplate" TargetType="{x:Type vm:State}">
<Grid Width="100" Height="60">
<!--
If I comment out the following Thumb
the drop event will only trigger once
If i leave it in then it triggers twice
Move Thumb is derived from thumb
-->
<local:MoveThumb Panel.ZIndex="99"
x:Name="StateViewModelMoveThumb"
DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}"
Opacity="0"/>
<Border Panel.ZIndex="98"
Margin="4"
Padding="4"
BorderBrush="white"
BorderThickness="2"
CornerRadius="5">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF59C7D4" Offset="0.5"/>
<GradientStop Color="#FF075A64" Offset="0"/>
<GradientStop Color="#FF00626E" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Content="{TemplateBinding StateName}"/>
</Border>
</Grid>
</ControlTemplate>
By the way what I trying to do is very loosely based on the following article: https://www.codeproject.com/Articles/22952/WPF-Diagram-Designer-Part-1

I tried your code, and I cannot reproduce your issue. Changes:
private void MainCanvas_Drop(object sender, DragEventArgs e)
{
var itemType = e.Data.GetData(typeof(string));
switch (itemType)
{
var pos = e.GetPosition(this.MainCanvas);
Border item = new Border()
{
Width = 10,
Height = 10,
Background = Brushes.Red
};
//item.Template = (ControlTemplate)FindResource("StateViewModelControlTemplate");
this.MainCanvas.Children.Add(item);
...
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed)
{
DataObject dragData = new DataObject();
dragData.SetData(DataFormats.StringFormat, "state");
DragDrop.DoDragDrop(this, dragData, DragDropEffects.Copy);
}
e.Handled = true;
}
and in xaml,
<Window ...
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Canvas x:Name="MainCanvas" Grid.Column="0"
DockPanel.Dock="Top"
Background="#666"
Height="600"
Margin="4"
AllowDrop="True"
DragEnter="MainCanvas_DragEnter"
Drop="MainCanvas_Drop"/>
<Grid Grid.Column="1" Background="Red"/>
</Grid>
Have not touch the other piece of code. Everytime I drag from the Grid to the Canvas, I get one Border item at the drop position.
So maybe the problem is not in the code you posted, but in the code left out or commented.

So I eventually got this working and here is how
Here is the front end xaml on the mainwindow
<cc:DiagramCanvas x:Name="MainCanvas"
DockPanel.Dock="Top"
Margin="0"
MinHeight="450"
AllowDrop="True" Background="White">
</cc:DiagramCanvas>
Here is the custom canvas object and the drag drop handler
public class DiagramCanvas : Canvas
{
public DiagramCanvas()
{
this.Drop += DoDrop;
this.DragEnter += MainCanvas_DragEnter;
}
readonly MainWindow mainWin = (MainWindow)Application.Current.MainWindow;
#region works dont touch
public void DoDrop(object sender, DragEventArgs e)
{
var mainWin = (MainWindow)App.Current.MainWindow;
DragDropHandler<StateVM>.Instance.Drop(sender, e);
}
private void MainCanvas_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.Text))
e.Effects = DragDropEffects.Copy;
else
e.Effects = DragDropEffects.None;
e.Handled = true;
}
#endregion
//other code here
}
Here is the drag drop handler singleton
public sealed class DragDropHandler<T> where T : Control
{
#region singleton
private static DragDropHandler<T> instance = null;
private static readonly object padlock = new object();
DragDropHandler()
{
}
public static DragDropHandler<T> Instance
{
get
{
lock (padlock)
{
if (instance == null)
{
instance = new DragDropHandler<T>();
}
return instance;
}
}
private set
{
instance = value;
}
}
#endregion
public static bool IsDragging { get; set; }
public static WrapPanel AllowedDragSource { get; set; }
readonly MainWindow mainWin = (MainWindow)Application.Current.MainWindow;
public static void CreateInstance(WrapPanel allowedSource)
{
if (DragDropHandler<T>.IsDragging == false)
{
instance = new DragDropHandler<T>();
DragDropHandler<T>.AllowedDragSource = allowedSource;
}
}
public void Drag(object sender, MouseEventArgs e)
{
if (sender as T == null
|| mainWin.Radio_EditStates.IsChecked == false
|| e.LeftButton != MouseButtonState.Pressed
|| IsDragging == true)
{
e.Handled = true;
return;
}
var item = (T)sender;
if (Control.ReferenceEquals(item.Parent, AllowedDragSource) == false)
{
e.Handled = true;
return;
}
IsDragging = true;
DragDrop.DoDragDrop(((StateVM)sender), new DataObject(((StateVM)sender)), DragDropEffects.Copy);
IsDragging = false;
e.Handled = true;
}
public void Drop(object sender, DragEventArgs e)
{
var mainWin = (MainWindow)App.Current.MainWindow;
if (IsDragging)
{
//TODO: Switch here to handle different shapes
var pos = e.GetPosition(mainWin.MainCanvas);
//
Canvas.SetLeft(item, pos.X.RoundDownTo10());
Canvas.SetTop(item, pos.Y.RoundDownTo10());
//update main win observalbe collections to inclue the item dropped
IsDragging = false;
e.Handled = true;
DestroyInstance();
}
}
private static void DestroyInstance()
{
DragDropHandler<T>.Instance = null;
}
}
Here is the on mouse move code for the item you are dragging
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed && Control.ReferenceEquals(this.Parent, mainWin.libraryContainer))
{
DragDropHandler<StateVM>.CreateInstance(mainWin.libraryContainer);
if (DragDropHandler<StateVM>.Instance != null)
{
DragDropHandler<StateVM>.Instance.Drag(this, e);
}
}
}

Related

How to scale a shape by selecting its stroke in wpf

I am new to WPF I want to scale ellipse by selecting its stroke. I have set IsManipulationEnabled=true but an event not triggering. Below is my code
<Path Stretch="Fill" Stroke="Black" ManipulationDelta="Path_ManipulationDelta"
IsManipulationEnabled="True" StrokeThickness="4">
<Path.Data>
<EllipseGeometry Center="0,0" RadiusX="200" RadiusY="200"/>
</Path.Data>
</Path>
Please Help.enter image description here
Here is some code that may give you some ideas:
In this sample, I'm using some basic mouse events MouseDown, MouseMove, and MouseUp so that I can detect when a user clicks on the Path, and when they start to drag the mouse.
XAML
<Window x:Class="WpfApp4.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp4"
Title="MainWindow"
Width="800"
Height="450"
UseLayoutRounding="True">
<Grid>
<Canvas x:Name="Canvas">
<Path x:Name="CirclePath"
MouseDown="OnMouseDown"
MouseMove="OnMouseMove"
MouseUp="OnMouseUp"
Stretch="Fill"
Stroke="Black"
StrokeThickness="4">
<Path.Data>
<EllipseGeometry x:Name="EllipseGeometry"
Center="0,0"
RadiusX="100"
RadiusY="100" />
</Path.Data>
</Path>
</Canvas>
</Grid>
</Window>
In the OnMouseDown handler, I check to see if the left mouse button is down and then I capture the mouse and get the position of the mouse relative to the Canvas.
Next, in the OnMouseMove handler, if the left button is still down - the user is dragging - I get the new mouse position and calculate the offset based on the old mouse position. Then I update the EllipseGeometry to reflect the mouse offset.
Finally, in the OnMouseUp handler, I release the mouse capture.
Code-Behind
using System.Windows;
using System.Windows.Input;
namespace WpfApp4
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private Point oldMousePosition;
public MainWindow()
{
InitializeComponent();
}
private void OnMouseDown(object sender, MouseButtonEventArgs e)
{
if(e.ChangedButton != MouseButton.Left) return;
Mouse.Capture(CirclePath);
oldMousePosition = e.GetPosition(Canvas);
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed) return;
var newMousePosition = e.GetPosition(Canvas);
var offset = newMousePosition - oldMousePosition;
EllipseGeometry.RadiusX += offset.X / 2;
EllipseGeometry.RadiusY += offset.Y / 2;
oldMousePosition = newMousePosition;
}
private void OnMouseUp(object sender, MouseButtonEventArgs e)
{
Mouse.Capture(null);
}
}
}
I hope this helps.
XAML
XAML
<Window x:Class="WidgetWpf.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:WidgetWpf"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Viewbox>
<Grid Name="MainGrid">
<Ellipse x:Name="DottedCircle" Width="200" Height="200" Stroke="White" StrokeThickness="2" Opacity="0.90" StrokeDashArray="4 4"
MouseDown="DottedCircle_MouseDown"
MouseMove="DottedCircle_MouseMove"
MouseUp="DottedCircle_MouseUp"
MouseEnter="DottedCircle_MouseEnter"
MouseLeave="DottedCircle_MouseLeave"
/>
</Grid>
</Viewbox>
</Window>
//Here is my code behind
public partial class MainWindow : Window
{
#region Variables
MatrixTransform transform;
Point OldMousePosition;
Point NewMousePosition;
double[] Dimensions = new double[2];
Rect rect = new Rect();
bool IsResizeMode;
bool IsDragAndDropMode;
#endregion
public MainWindow()
{
InitializeComponent();
}
private void DottedCircle_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton != MouseButton.Left) return;
Mouse.Capture(DottedCircle);
OldMousePosition = e.GetPosition(MainGrid);
}
private void DottedCircle_MouseMove(object sender, MouseEventArgs e)
{
DottedCircle.ToolTip = e.GetPosition(MainGrid);
if (e.LeftButton != MouseButtonState.Pressed) return;
var NewMousePosition = e.GetPosition(MainGrid);
var offset = NewMousePosition-OldMousePosition;
#region working by co-ordinate
//get center of grid
double dicisionPoint=0.0 ;
double CP_X = MainGrid.ActualWidth / 2;
double CP_Y = MainGrid.ActualHeight / 2;
//1 st co-ordinate
if(NewMousePosition.X>CP_X && NewMousePosition.Y<CP_Y)
{
dicisionPoint = offset.X;
}
//2nd cordinate
else if (NewMousePosition.X < CP_X && NewMousePosition.Y < CP_Y)
{
dicisionPoint = -offset.X;
}
else if (NewMousePosition.X < CP_X && NewMousePosition.Y > CP_Y)
{
dicisionPoint = offset.Y;
}
else if (NewMousePosition.X > CP_X && NewMousePosition.Y > CP_Y)
{
dicisionPoint = offset.Y;
}
if (DottedCircle.Width+ dicisionPoint < InnerCircle.Width)
{
DottedCircle.Fill = new SolidColorBrush(Colors.Transparent);
DottedCircle.Width += dicisionPoint;
DottedCircle.Height += dicisionPoint;
}
else if (DottedCircle.Width+ dicisionPoint>= InnerCircle.Width) { DottedCircle.Fill = new SolidColorBrush(Colors.Red); }
#endregion
OldMousePosition = NewMousePosition;
DottedCircle.ToolTip = offset.X+ "__" + offset.Y;
}
private void DottedCircle_MouseUp(object sender, MouseButtonEventArgs e)
{
Mouse.Capture(null);
DottedCircle.Style = null;
}
private void DottedCircle_MouseEnter(object sender, MouseEventArgs e)
{
DottedCircle.Stroke = new SolidColorBrush( Colors.Blue);
DottedCircle.Style = (Style)Application.Current.Resources["DiffPathStyle"];
}
private void DottedCircle_MouseLeave(object sender, MouseEventArgs e)
{
DottedCircle.Stroke = new SolidColorBrush(Colors.White);
DottedCircle.Style = null;
}
}

WPF bind GradientStopCollection

How to bind GradientStopCollection with some GradientStops in ViewModel
<Rectangle Width="30" Height="200">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStopCollection ????? />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
The GradientBrush (baseclass of LinearGradientBrush) is defined with [ContentProperty("GradientStops")], which means, that the GradientStopCollection can be either set directly as content or explicitely as GradientStops property.
So you can define a GradientStopCollection in resources or code behind and then bind it to your brush.
A little example XAML:
<Grid x:Name="grid1">
<Rectangle Width="30" Height="200" Margin="20">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1"
GradientStops="{Binding}">
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
Code Behind:
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
GradientStopCollection c = new GradientStopCollection();
c.Add(new GradientStop(Colors.Red, 0.0));
c.Add(new GradientStop(Colors.Green, 0.5));
c.Add(new GradientStop(Colors.Yellow, 1.0));
grid1.DataContext = c;
}
I don't think you can manually add/remove individual collection items without code behind, but as said, you should be able to define / replace the whole collection in XAML as well.
Edit
So let's suppose you want to decouple your viewmodel a bit more:
public class GradientTransferObject
{
public Color Color { get; set; }
public double Offset { get; set; }
}
// ....
public ObservableCollection<GradientTransferObject> Gradients { get; set; }
Now there is a need to translate the source items into an GradientStopCollection. This can be done with something similar to a CollectionViewSource - an object that can be hosted as resource, takes an items source and provides an item collection.
public class GradientProvider : Freezable
{
// the resulting items collection
public GradientStopCollection GradientItems
{
get { return (GradientStopCollection)GetValue(GradientItemsProperty); }
private set { SetValue(GradientItemsPropertyKey, value); }
}
private static readonly DependencyPropertyKey GradientItemsPropertyKey =
DependencyProperty.RegisterReadOnly("GradientItems", typeof(GradientStopCollection), typeof(GradientProvider), new PropertyMetadata(null));
public static readonly DependencyProperty GradientItemsProperty = GradientItemsPropertyKey.DependencyProperty;
// the items source from viewmodel data
public IEnumerable<GradientTransferObject> GradientItemsSource
{
get { return (IEnumerable<GradientTransferObject>)GetValue(GradientItemsSourceProperty); }
set { SetValue(GradientItemsSourceProperty, value); }
}
public static readonly DependencyProperty GradientItemsSourceProperty =
DependencyProperty.Register("GradientItemsSource", typeof(IEnumerable<GradientTransferObject>), typeof(GradientProvider),
new PropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var self = d as GradientProvider;
if (e.OldValue == e.NewValue)
{
return;
}
if (e.OldValue is ObservableCollection<GradientTransferObject>)
{
var c = e.OldValue as ObservableCollection<GradientTransferObject>;
c.CollectionChanged -= self.CollectionChanged;
}
if (e.NewValue is ObservableCollection<GradientTransferObject>)
{
var c = e.NewValue as ObservableCollection<GradientTransferObject>;
c.CollectionChanged += self.CollectionChanged;
}
self.UpdateItems();
}
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UpdateItems();
}
private void UpdateItems()
{
GradientItems = new GradientStopCollection(GradientItemsSource.Select(x => new GradientStop(x.Color, x.Offset)));
}
protected override Freezable CreateInstanceCore()
{
return new GradientProvider();
}
}
The updated xaml example:
<Grid x:Name="grid1">
<Grid.Resources>
<local:GradientProvider x:Key="gradientSource" GradientItemsSource="{Binding}"/>
</Grid.Resources>
<Rectangle Width="30" Height="200" Margin="20">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1"
GradientStops="{Binding GradientItems,Source={StaticResource gradientSource}}">
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Button VerticalAlignment="Top" HorizontalAlignment="Left" Margin="5" Content="AddGradient" Click="Button_Click"/>
</Grid>
The updated code, dynamically changing the gradient collection on button click:
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Gradients = new ObservableCollection<GradientTransferObject>()
{
new GradientTransferObject{ Color = Colors.Red, Offset = 0.0},
new GradientTransferObject{ Color = Colors.Yellow, Offset = 1.0},
};
grid1.DataContext = Gradients;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Gradients.Add(new GradientTransferObject { Color = Colors.Green, Offset = 0.5 });
}
So as you see, you are free to take it easy with direct usage of GradientStopCollection in the viewmodel or go for full abstraction where your viewmodel source and your view work on completely different data types.

WPF popup with arrow head style

I would like to implement a popup style that looks like the following design:
The gray square represents the UIElement that shows the popup when it is clicked. The popup style is just a border (the easy part) with an arrow head that points to the center of the target element (the hard part). Also the aligment is important, when a control is placed on the right part of the window, the popup should be aligned to the right, otherwise to the left.
Is there an example or some documentation that instruct me how to proceed?
Okay, I have a solution for this. It's frustratingly complex.
If you're just after a plain pop-up, just with a tail, you can probably use chunks of this (ActualLayout and UpdateTail logic). If you're after the whole Help-Tip Experience™ you're in for an unpleasant ride.
I do think that it might be better to go down the Adorner route though (and I'm thinking of re-working this to use adorers). I've spotted some issues and it's still in work. Using pop-ups causes them to appear in the designer on the top of other windows and it's really annoying. I've also noticed they are not positioned correctly on some computers for some weird reasons (but none where I've got Visual Studio installed to properly debug).
It produces something like this:
With the following criteria:
Only a single help tip can be displayed on the screen at each time
If the user changes tab, and the control a help-tip is attached to is no longer visible, the help tip dissapears and the next help-tip is displayed
Once closed, a help-tip of that type won't be displayed again
Help tips can be turned off through one central option
Okay. So, the actual help tip is a usercontrol that's completely transparent and added to the UI. It has a pop-up that's managed using a static class. Here's the control:
<UserControl x:Class="...HelpPopup"
d:DesignHeight="0" d:DesignWidth="0">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</UserControl.Resources>
<Canvas>
<Popup x:Name="Popup"
d:DataContext="{d:DesignInstance {x:Null}}"
DataContext="{Binding HelpTip, ElementName=userControl}"
StaysOpen="True" PopupAnimation="Fade"
AllowsTransparency="True"
materialDesign:ShadowAssist.ShadowDepth="Depth3"
Placement="{Binding Placement, ElementName=userControl}"
HorizontalOffset="-10"
VerticalOffset="{Binding VerticalOffset, ElementName=userControl}">
<Grid Margin="0,0,0,0" SnapsToDevicePixels="True">
<Canvas Margin="10">
<local:RoundedCornersPolygon Fill="{StaticResource PrimaryHueDarkBrush}"
SnapsToDevicePixels="True"
ArcRoundness="4"
Points="{Binding PolygonPath, ElementName=userControl}"
Effect="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Popup}, Path=(materialDesign:ShadowAssist.ShadowDepth), Converter={x:Static converters:ShadowConverter.Instance}}"/>
</Canvas>
<Border BorderBrush="Transparent" BorderThickness="10,25,10,25">
<Grid x:Name="PopupChild">
<materialDesign:ColorZone Mode="PrimaryDark" Margin="5">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="AUTO"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Useful Tip"
FontWeight="Bold"
Margin="2,0,0,0"
Grid.ColumnSpan="2"
VerticalAlignment="Center"/>
<Button Style="{StaticResource MaterialDesignToolButton}" Click="CloseButton_Click" Grid.Column="1" Margin="0" Padding="0" Height="Auto">
<Button.Content>
<materialDesign:PackIcon Kind="CloseCircle" Height="20" Width="20" Foreground="{StaticResource PrimaryHueLightBrush}"/>
</Button.Content>
</Button>
</Grid>
<TextBlock Text="{Binding Message}"
TextWrapping="Wrap"
MaxWidth="300"
Margin="2,4,2,4"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Close" Padding="8,2" Height="Auto" Click="CloseButton_Click"
Margin="2"
Style="{StaticResource MaterialDesignFlatButtonInverted}"/>
<Button Content="Never show again"
Margin="2"
Padding="8,2"
Height="Auto"
Click="NeverShowButton_Click"
Style="{StaticResource MaterialDesignFlatButtonInverted}"/>
</StackPanel>
</StackPanel>
</materialDesign:ColorZone>
</Grid>
</Border>
</Grid>
</Popup>
</Canvas>
</UserControl>
You can change this to style how you want. I used a custom rounded polygon class and MaterialDesign colour zones. Replace these however you want.
Now, the code behind is... well, there's a lot of it, and it's not pleasant:
public enum ActualPlacement { TopLeft, TopRight, BottomLeft, BottomRight }
/// <summary>
/// Interaction logic for HelpPopup.xaml
/// </summary>
public partial class HelpPopup : UserControl, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ActualPlacement actualPlacement = ActualPlacement.TopRight;
public ActualPlacement ActualPlacement
{
get { return actualPlacement; }
internal set
{
if (actualPlacement != value)
{
if (actualPlacement == ActualPlacement.BottomLeft || ActualPlacement == ActualPlacement.BottomRight)
{
Console.WriteLine("-10");
VerticalOffset = 10;
}
else if (actualPlacement == ActualPlacement.TopLeft || ActualPlacement == ActualPlacement.TopRight)
{
VerticalOffset = -10;
Console.WriteLine("10");
}
actualPlacement = value;
UpdateTailPath();
NotifyOfPropertyChange("ActualPlacement");
}
}
}
public void UpdateTailPath()
{
double height = PopupChild.ActualHeight + 30;
double width = PopupChild.ActualWidth;
switch (actualPlacement)
{
case ActualPlacement.TopRight:
polygonPath = "0.5,15.5 " + (width - 0.5) + ",15.5 " + (width - 0.5) + "," + (height - 15.5) +
" 15.5," + (height - 15.5) + " 0.5," + height + " 0.5,15.5"; ;
break;
case ActualPlacement.TopLeft:
polygonPath = "0.5,15.5 " + (width - 0.5) + ",15.5 " + (width - 0.5) + "," + height + " " + (width - 15.5) + "," + (height - 15.5) +
" 0.5," + (height - 15.5) + " 0.5,15.5";
break;
case ActualPlacement.BottomRight:
polygonPath = "0.5,0.5 15.5,15.5 " + (width - 0.5) + ",15.5 " + (width - 0.5) + "," + (height - 15.5) +
" 0.5," + (height - 15.5) + " 0.5,0.5";
break;
case ActualPlacement.BottomLeft:
polygonPath = "0.5,15.5 " + (width - 15.5) + ",15.5 " + (width - 0.5) + ",0.5 " + (width - 0.5) + "," + (height - 15.5) +
" 0.5," + (height - 15.5) + " 0.5,15.5";
break;
}
NotifyOfPropertyChange("PolygonPath");
}
private String polygonPath;
public String PolygonPath
{
get { return polygonPath; }
}
public PlacementMode Placement
{
get { return (PlacementMode)GetValue(PlacementProperty); }
set { SetValue(PlacementProperty, value); }
}
// Using a DependencyProperty as the backing store for Placement. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PlacementProperty =
DependencyProperty.Register("Placement", typeof(PlacementMode), typeof(HelpPopup), new PropertyMetadata(PlacementMode.Top));
public int VerticalOffset
{
get { return (int)GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
// Using a DependencyProperty as the backing store for VerticalOffset. This enables animation, styling, binding, etc...
public static readonly DependencyProperty VerticalOffsetProperty =
DependencyProperty.Register("VerticalOffset", typeof(int), typeof(HelpPopup), new PropertyMetadata(-10));
public HelpTip HelpTip
{
get { return (HelpTip)GetValue(HelpTipProperty); }
set { SetValue(HelpTipProperty, value); }
}
// Using a DependencyProperty as the backing store for Message. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HelpTipProperty =
DependencyProperty.Register("HelpTip", typeof(HelpTip), typeof(HelpPopup), new PropertyMetadata(new HelpTip() { Message = "No help message found..." }, HelpTipChanged));
private static void HelpTipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((d as HelpPopup).HelpTipOnScreenInstance == null)
{
(d as HelpPopup).HelpTipOnScreenInstance = new HelpTipOnScreenInstance((d as HelpPopup));
}
(d as HelpPopup).HelpTipOnScreenInstance.HelpTip = (e.NewValue as HelpTip);
}
private static void HelpTipOnScreenInstance_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
HelpTipOnScreenInstance htosi = sender as HelpTipOnScreenInstance;
if (e.PropertyName.Equals(nameof(htosi.IsOpen)))
{
//open manually to avoid stupid COM errors
if (htosi != null)
{
try
{
htosi.HelpPopup.Popup.IsOpen = htosi.IsOpen;
}
catch (System.ComponentModel.Win32Exception ex)
{
Canvas parent = htosi.HelpPopup.Popup.Parent as Canvas;
htosi.HelpPopup.Popup.IsOpen = false;
parent.Children.Remove(htosi.HelpPopup.Popup);
Application.Current.Dispatcher.BeginInvoke(new Action(() => {
htosi.HelpPopup.Popup.IsOpen = true;
parent.Children.Add(htosi.HelpPopup.Popup);
htosi.HelpPopup.UpdatePositions();
}), DispatcherPriority.SystemIdle);
}
}
}
}
private HelpTipOnScreenInstance helpTipOnScreenInstance;
public HelpTipOnScreenInstance HelpTipOnScreenInstance
{
get { return helpTipOnScreenInstance; }
set
{
if (helpTipOnScreenInstance != value)
{
if (helpTipOnScreenInstance != null)
{
HelpTipOnScreenInstance.PropertyChanged -= HelpTipOnScreenInstance_PropertyChanged;
}
helpTipOnScreenInstance = value;
HelpTipOnScreenInstance.PropertyChanged += HelpTipOnScreenInstance_PropertyChanged;
NotifyOfPropertyChange("HelpTipOnScreenInstance");
}
}
}
private double popupX;
public double PopupX
{
get { return popupX; }
set
{
if (popupX != value)
{
popupX = value;
NotifyOfPropertyChange("PopupX");
}
}
}
private double popupY;
public double PopupY
{
get { return popupY; }
set
{
if (popupY != value)
{
popupY = value;
NotifyOfPropertyChange("PopupY");
}
}
}
private void NotifyOfPropertyChange(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public HelpPopup()
{
InitializeComponent();
// Wire up the Loaded handler instead
this.Loaded += new RoutedEventHandler(View1_Loaded);
this.Unloaded += HelpPopup_Unloaded;
Popup.Opened += Popup_Opened;
//PopupChild.LayoutUpdated += HelpPopup_LayoutUpdated;
PopupChild.SizeChanged += HelpPopup_SizeChanged;
UpdatePositions();
}
private void Popup_Opened(object sender, EventArgs e)
{
UpdateTail();
UpdateTailPath();
}
private void HelpPopup_SizeChanged(object sender, SizeChangedEventArgs e)
{
Console.WriteLine(HelpTip.Message + ": " + e.PreviousSize.ToString() + " to " + e.NewSize.ToString());
UpdateTail();
UpdateTailPath();
}
private void HelpPopup_Unloaded(object sender, RoutedEventArgs e)
{
//don't waste resources on never show popups
if (HelpTip.NeverShow)
{
return;
}
HelpTipOnScreenInstance.IsOnscreen = false;
}
/// Provides a way to "dock" the Popup control to the Window
/// so that the popup "sticks" to the window while the window is dragged around.
void View1_Loaded(object sender, RoutedEventArgs e)
{
//don't waste resources on never show popups
if (HelpTip.NeverShow)
{
return;
}
//wait for a few seconds, then set this to on-screen
HelpTipOnScreenInstance.IsOnscreen = true;
//update so tail is facing right direction
UpdateTail();
Window w = Window.GetWindow(this);
// w should not be Null now!
if (null != w)
{
w.LocationChanged += delegate (object sender2, EventArgs args)
{
// "bump" the offset to cause the popup to reposition itself
// on its own
UpdatePositions();
};
// Also handle the window being resized (so the popup's position stays
// relative to its target element if the target element moves upon
// window resize)
w.SizeChanged += delegate (object sender3, SizeChangedEventArgs e2)
{
UpdatePositions();
};
}
}
private void UpdatePositions()
{
var offset = Popup.HorizontalOffset;
Popup.HorizontalOffset = offset + 1;
Popup.HorizontalOffset = offset;
UpdateTail();
}
private void UpdateTail()
{
UIElement container = VisualTreeHelper.GetParent(this) as UIElement;
Point relativeLocation = PopupChild.TranslatePoint(new Point(5, 5), container); //It HAS(!!!) to be this.Child
if (relativeLocation.Y < 0)
{
if (relativeLocation.X < -(PopupChild.ActualWidth-5 / 2))
{
ActualPlacement = ActualPlacement.TopLeft;
}
else
{
ActualPlacement = ActualPlacement.TopRight;
}
}
else
{
if (relativeLocation.X < -(PopupChild.ActualWidth-5 / 2))
{
ActualPlacement = ActualPlacement.BottomLeft;
}
else
{
ActualPlacement = ActualPlacement.BottomRight;
}
}
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
lock (HelpTip.Lock)
{
HelpTip.Closed = true;
HelpTipOnScreenInstance.IsOpen = false;
}
}
private void NeverShowButton_Click(object sender, RoutedEventArgs e)
{
lock (HelpTip.Lock)
{
HelpTip.Closed = true;
HelpTip.NeverShow = true;
HelpTipOnScreenInstance.IsOpen = false;
}
}
}
Things to note.
There's "ActualPlacement" to manage the actual placement of the popup, as setting placement is just an advisory to WPF.
UpdateTailPath() is re-drawing the polygon to get the tail in the
correct position after placement has changed.
We have both a HelpTip class that stores the information (title,
content etc), and HelpTipOnScreenInstance which controls whether it's
on screen. The reason for this is we can have multiple help-tip of
the same type on screen and only want to display one.
Various listeners for popup events to trigger tail updates.
We attach to the load and unload events of the usercontrol. This
allows us to keep track of whether the control is on the screen and
whether a help tip should or should not be displayed
(HelpTipOnScreenInstance.IsOnscreen = true).
We also listen for window change events so we can update the position
of the pop-up if the window is resized or moved.
Now, HelpTipOnScreenInstance, and HelpTip:
public class HelpTipOnScreenInstance : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public object Lock = new Object();
private void NotifyOfPropertyChange(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
//handler(this, new PropertyChangedEventArgs(propertyName));
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private HelpTip helpTip;
public HelpTip HelpTip
{
get { return helpTip; }
set
{
if (helpTip != value)
{
helpTip = value;
NotifyOfPropertyChange("HelpTip");
}
}
}
private bool isOpen = false;
public bool IsOpen
{
get { return isOpen; }
set
{
if (isOpen != value)
{
isOpen = value;
Console.WriteLine("Opening " + HelpTip.Message);
NotifyOfPropertyChange("IsOpen");
}
}
}
private bool isOnscreen = false;
public bool IsOnscreen
{
get { return isOnscreen; }
set
{
if (isOnscreen != value)
{
isOnscreen = value;
NotifyOfPropertyChange("IsOnscreen");
}
}
}
private HelpPopup helpPopup;
public HelpPopup HelpPopup
{
get { return helpPopup; }
set
{
if (helpPopup != value)
{
helpPopup = value;
NotifyOfPropertyChange("HelpPopup");
}
}
}
public HelpTipOnScreenInstance(HelpPopup helpPopup)
{
HelpPopup = helpPopup;
HelpTipManager.AddHelpTip(this);
}
}
public class HelpTip : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public object Lock = new Object();
private void NotifyOfPropertyChange(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
//handler(this, new PropertyChangedEventArgs(propertyName));
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private String id;
public String ID
{
get { return id; }
set { id = value; }
}
private String message;
public String Message
{
get { return message; }
set
{
if (message != value)
{
message = value;
NotifyOfPropertyChange("Message");
}
}
}
private bool closed;
public bool Closed
{
get { return closed; }
set
{
if (closed != value)
{
closed = value;
NotifyOfPropertyChange("Closed");
}
}
}
public bool NeverShow { get; set; }
}
And then a static manager class that keeps track of what's on screen and what's not, and chooses who get's displayed next:
public static class HelpTipManager
{
public static object Lock = new Object();
private static bool displayHelpTips = false;
public static bool DisplayHelpTips
{
get { return displayHelpTips; }
set {
if (displayHelpTips != value)
{
displayHelpTips = value;
if (displayHelpTips)
{
//open next!
OpenNext();
}
else
{
//stop displaying all
foreach(HelpTipOnScreenInstance helpTip in helpTipsOnScreen)
{
lock (helpTip.HelpTip.Lock)
{
helpTip.IsOpen = false;
}
}
}
}
}
}
private static List<HelpTipOnScreenInstance> helpTips = new List<HelpTipOnScreenInstance>();
private static List<HelpTipOnScreenInstance> helpTipsOnScreen = new List<HelpTipOnScreenInstance>();
private static bool supressOpenNext = false;
public static void AddHelpTip(HelpTipOnScreenInstance helpTip)
{
helpTip.PropertyChanged += HelpTip_PropertyChanged;
helpTips.Add(helpTip);
}
private static void HelpTip_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
HelpTipOnScreenInstance helpTip = sender as HelpTipOnScreenInstance;
if (helpTip != null)
{
//is this on screen or not?
switch (e.PropertyName)
{
case "IsOnscreen":
//Update our onscreen lists and perform related behaviour
if (helpTip.IsOnscreen)
{
AddedToScreen(helpTip);
}
else
{
RemovedFromScreen(helpTip);
}
break;
case "IsOpen":
lock (helpTip.Lock)
{
if (!supressOpenNext)
{
if (!helpTip.IsOpen)
{
OpenNext();
}
}
}
break;
}
}
}
private static void OpenNext()
{
if (DisplayHelpTips)
{
if (helpTipsOnScreen.Count > 0)
{
//check if none of them are open
if (helpTipsOnScreen.Count(ht => ht.IsOpen) == 0)
{
//open the first that's not been closed!
HelpTipOnScreenInstance firstNotClosed = helpTipsOnScreen.FirstOrDefault(ht => !ht.HelpTip.Closed);
if (firstNotClosed != null)
{
lock (firstNotClosed.Lock)
{
firstNotClosed.IsOpen = true;
}
}
}
}
}
}
private static void AddedToScreen(HelpTipOnScreenInstance helpTip)
{
lock (Lock)
{
helpTipsOnScreen.Add(helpTip);
OpenNext();
}
}
private static void RemovedFromScreen(HelpTipOnScreenInstance helpTip)
{
lock (Lock)
{
helpTipsOnScreen.Remove(helpTip);
supressOpenNext = true;
helpTip.IsOpen = false;
//OpenNext();
supressOpenNext = false;
}
}
}
So how to use it? You can add help tip data in your generic.xaml or a resource library like this:
<controls:HelpTip x:Key="KPIGraphMenu" ID="KPIGraphMenu" Message="Right click to change the colour, remove, or move KPI to view as a stacked trace. KPI can also be dragged onto other charts of any type."/>
and use them in the actual application like this, I like to overlay them in a grid with the control they're associated with, using the Alignment to determine where the tail points to:
<controls:HelpPopup HelpTip="{StaticResource KPIGraphMenu}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
I have used CustomPopupPlacementCallback Delegate . I have even considered vertical shifting of your arrow. So, now in this example below, arrow shifts left/right, up/down.
One can use this sample as it is.
Window1.xaml
<Window ...>
<Grid>
<Button Click="Btn_Click" Width="110" Height="25" Content="Button" HorizontalAlignment="Left" Margin="437,26,0,0" VerticalAlignment="Top"/>
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" Margin="10,90,0,0" VerticalAlignment="Top" Width="75"/>
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" Margin="139,146,0,0" VerticalAlignment="Top" Width="75"/>
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" Margin="180,0,0,0" VerticalAlignment="Top" Width="74"/>
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" Margin="224,333,0,0" VerticalAlignment="Top" Width="76"/>
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Right" VerticalAlignment="Top" Width="75"/>
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="75" />
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="75" />
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" />
<Popup x:Name="Popup1" Placement="Custom" StaysOpen="False" Opened="Popup1_Opened">
<Grid x:Name="Grd" Width="300" Height="100" Background="AliceBlue">
<Canvas x:Name="Cnv">
<Path x:Name="TopArrow" Canvas.Left="50" Canvas.Top="25" Margin="5" Data="M0,0 L-5,-5 L-10,0 z" Fill="Black" Stroke="Black" StrokeThickness="2"/>
<TextBlock Canvas.Top="35" FontSize="18" x:Name="Tb1"/>
</Canvas>
</Grid>
</Popup>
</Grid>
</Window>
Window1.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Shapes;
namespace ...
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
Popup1.CustomPopupPlacementCallback =
new CustomPopupPlacementCallback(placePopup);
}
public CustomPopupPlacement[] placePopup(Size popupSize,
Size targetSize,
Point offset)
{
CustomPopupPlacement placement2 =
new CustomPopupPlacement(new Point(-(popupSize.Width - targetSize.Width / 2), targetSize.Height), PopupPrimaryAxis.Vertical);
CustomPopupPlacement placement1 =
new CustomPopupPlacement(new Point(targetSize.Width / 2, targetSize.Height), PopupPrimaryAxis.Vertical);
CustomPopupPlacement placement3 =
new CustomPopupPlacement(new Point(targetSize.Width/2, -popupSize.Height), PopupPrimaryAxis.Horizontal);
CustomPopupPlacement placement4 =
new CustomPopupPlacement(new Point(-(popupSize.Width - targetSize.Width/2), -popupSize.Height), PopupPrimaryAxis.Horizontal);
CustomPopupPlacement[] ttplaces =
new CustomPopupPlacement[] { placement1, placement2, placement3, placement4 };
return ttplaces;
}
private void Btn_Click(object sender, RoutedEventArgs e)
{
Popup1.PlacementTarget = sender as Button;
Popup1.IsOpen = true;
}
private void Popup1_Opened(object sender, EventArgs e)
{
Path arrow = ((Path)Popup1.FindName("TopArrow"));
Grid grd = ((Grid)Popup1.FindName("Grd"));
UIElement elem = (UIElement)Popup1.PlacementTarget;
Point elem_pos_lefttop = elem.PointToScreen(new Point(0, 0));
Point popup_pos_lefttop = grd.PointToScreen(new Point(0, 0));
if ( (elem_pos_lefttop.Y < popup_pos_lefttop.Y )
&&
((elem_pos_lefttop.X > popup_pos_lefttop.X))
)
{
Canvas.SetLeft(arrow, 280);
Canvas.SetTop(arrow, 25);
}
if ((elem_pos_lefttop.Y < popup_pos_lefttop.Y)
&&
((elem_pos_lefttop.X < popup_pos_lefttop.X))
)
{
Canvas.SetLeft(arrow, 30);
Canvas.SetTop(arrow, 25);
}
if ((elem_pos_lefttop.Y > popup_pos_lefttop.Y)
&&
((elem_pos_lefttop.X > popup_pos_lefttop.X))
)
{
Canvas.SetLeft(arrow, 280);
Canvas.SetTop(arrow, 90);
}
if ((elem_pos_lefttop.Y > popup_pos_lefttop.Y)
&&
((elem_pos_lefttop.X < popup_pos_lefttop.X))
)
{
Canvas.SetLeft(arrow, 30);
Canvas.SetTop(arrow, 90);
}
Tb1.Text = String.Format("Element = {0} \r\n Popup = {1}", elem_pos_lefttop, popup_pos_lefttop);
}
}
}
Please tell if this solves your issue.

WPF ListBox automatic scrolling start and stop behavior

I've been trying to improve the behavior of the WPF ListBox control in the following way: The ListBox below automatically scrolls to the bottom as new items are added. It does this using the ScrollToBottom function shown. Using the preview events shown, if the user clicks an item, it stops scrolling, even if more items are added. (It would be obnoxious to let it keep scrolling!) If the user manually scrolls with the mouse or wheel, then it stops scrolling in the same way.
Right now I have a button in the code below that starts automatic scrolling again.
My question is this: How can I start off automatic scrolling if the user either scrolls the listbox all the way down to the bottom, or does the equivalent with the mouse wheel or keyboard. This is how my old Borland listboxes used to work out of the box.
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
// Note requires .NET framework 4.5
namespace MMP
{
public partial class MainWindow : Window
{
public ObservableCollection<String> data { get; set; }
public MainWindow()
{
InitializeComponent();
data = new ObservableCollection<String>();
DataContext = this;
BeginAddingItems();
}
private async void BeginAddingItems()
{
await Task.Factory.StartNew(() =>
{
for (int i = 0; i < Int32.MaxValue; ++i)
{
if (i > 20)
Thread.Sleep(1000);
AddToList("Added " + i.ToString());
}
});
}
void AddToList(String item)
{
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new Action(() => { data.Add(item); ScrollToBottom(); }));
}
bool autoScroll = true;
public void ScrollToBottom()
{
if (!autoScroll)
return;
if (listbox.Items.Count > 0)
listbox.ScrollIntoView(listbox.Items[listbox.Items.Count - 1]);
}
private void listbox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
autoScroll = false;
Console.WriteLine("PreviewMouseDown: setting autoScroll to false");
}
private void listbox_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
Console.WriteLine("PreviewMouseWheel: setting autoScroll to false");
autoScroll = false;
}
private void startButton_Click(object sender, RoutedEventArgs e)
{
ScrollToBottom(); // Catch up with the current last item.
Console.WriteLine("startButton_Click: setting autoScroll to true");
autoScroll = true;
}
private void listbox_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
// Can this be useful?
}
}
}
<Window x:Class="MMP.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Test Scrolling"
FontFamily="Verdana"
Width="400" Height="250"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox x:Name="listbox" Grid.Row="0"
PreviewMouseWheel="listbox_PreviewMouseWheel"
PreviewMouseDown="listbox_PreviewMouseDown"
ItemsSource="{Binding data}" ScrollViewer.ScrollChanged="listbox_ScrollChanged"
>
</ListBox>
<StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right">
<Button x:Name="startButton" Click="startButton_Click" MinWidth="80" >Auto Scroll</Button>
</StackPanel>
</Grid>
</Window>
The desired listbox behavior was achieved using the following code, with kind thanks to Roel for providing the initial Behavior<> framework above.
This is a sample project that contains the behavior code, along with a minimal WPF window that can be used to test the interactivity.
The test window contains a ListBox, to which items are added asynchronously via a background task. The important points of the behavior are as follows:
List box automatically scrolls to show new items as they are added asynchronously.
A user interaction with the listbox stops automatic scrolling - AKA obnoxious behavior.
Once finished interacting, to continue automatic scrolling, user drags the scroll bar to the bottom and lets go, or uses the mouse wheel or keyboard to do the same. This indicates that the user wants automatic scrolling to resume.
AutoScrolBehavior.cs:
using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
using System.Windows.Media;
namespace BehaviorTest.Code
{
// List box automatically scrolls to show new items as they are added asynchronously.
// A user interaction with the listbox stops automatic scrolling - AKA obnoxious behavior.
// Once finished interacting, to continue automatic scrolling, drag the scroll bar to
// the bottom and let go, or use the mouse wheel or keyboard to do the same.
// This indicates that the user wants automatic scrolling to resume.
public class AutoScrollBehavior : Behavior<ListBox>
{
private ScrollViewer scrollViewer;
private bool autoScroll = true;
private bool justWheeled = false;
private bool userInteracting = false;
protected override void OnAttached()
{
AssociatedObject.Loaded += AssociatedObjectOnLoaded;
AssociatedObject.Unloaded += AssociatedObjectOnUnloaded;
}
private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs routedEventArgs)
{
if (scrollViewer != null)
{
scrollViewer.ScrollChanged -= ScrollViewerOnScrollChanged;
}
AssociatedObject.SelectionChanged -= AssociatedObjectOnSelectionChanged;
AssociatedObject.ItemContainerGenerator.ItemsChanged -= ItemContainerGeneratorItemsChanged;
AssociatedObject.GotMouseCapture -= AssociatedObject_GotMouseCapture;
AssociatedObject.LostMouseCapture -= AssociatedObject_LostMouseCapture;
AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
scrollViewer = null;
}
private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
scrollViewer = GetScrollViewer(AssociatedObject);
if (scrollViewer != null)
{
scrollViewer.ScrollChanged += ScrollViewerOnScrollChanged;
AssociatedObject.SelectionChanged += AssociatedObjectOnSelectionChanged;
AssociatedObject.ItemContainerGenerator.ItemsChanged += ItemContainerGeneratorItemsChanged;
AssociatedObject.GotMouseCapture += AssociatedObject_GotMouseCapture;
AssociatedObject.LostMouseCapture += AssociatedObject_LostMouseCapture;
AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel;
}
}
private static ScrollViewer GetScrollViewer(DependencyObject root)
{
int childCount = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < childCount; ++i)
{
DependencyObject child = VisualTreeHelper.GetChild(root, i);
ScrollViewer sv = child as ScrollViewer;
if (sv != null)
return sv;
return GetScrollViewer(child);
}
return null;
}
void AssociatedObject_GotMouseCapture(object sender, System.Windows.Input.MouseEventArgs e)
{
// User is actively interacting with listbox. Do not allow automatic scrolling to interfere with user experience.
userInteracting = true;
autoScroll = false;
}
void AssociatedObject_LostMouseCapture(object sender, System.Windows.Input.MouseEventArgs e)
{
// User is done interacting with control.
userInteracting = false;
}
private void ScrollViewerOnScrollChanged(object sender, ScrollChangedEventArgs e)
{
// diff is exactly zero if the last item in the list is visible. This can occur because of scroll-bar drag, mouse-wheel, or keyboard event.
double diff = (scrollViewer.VerticalOffset - (scrollViewer.ExtentHeight - scrollViewer.ViewportHeight));
// User just wheeled; this event is called immediately afterwards.
if (justWheeled && diff != 0.0)
{
justWheeled = false;
autoScroll = false;
return;
}
if (diff == 0.0)
{
// then assume user has finished with interaction and has indicated through this action that scrolling should continue automatically.
autoScroll = true;
}
}
private void ItemContainerGeneratorItemsChanged(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset)
{
// An item was added to the listbox, or listbox was cleared.
if (autoScroll && !userInteracting)
{
// If automatic scrolling is turned on, scroll to the bottom to bring new item into view.
// Do not do this if the user is actively interacting with the listbox.
scrollViewer.ScrollToBottom();
}
}
}
private void AssociatedObjectOnSelectionChanged(object sender, SelectionChangedEventArgs selectionChangedEventArgs)
{
// User selected (clicked) an item, or used the keyboard to select a different item.
// Turn off automatic scrolling.
autoScroll = false;
}
void AssociatedObject_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
// User wheeled the mouse.
// Cannot detect whether scroll viewer right at the bottom, because the scroll event has not occurred at this point.
// Same for bubbling event.
// Just indicated that the user mouse-wheeled, and that the scroll viewer should decide whether or not to stop autoscrolling.
justWheeled = true;
}
}
}
MainWindow.xaml.cs:
using BehaviorTest.Code;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Threading;
namespace BehaviorTest
{
public partial class MainWindow : Window
{
public ObservableCollection<String> data { get; set; }
public MainWindow()
{
InitializeComponent();
data = new ObservableCollection<String>();
DataContext = this;
Interaction.GetBehaviors(listbox).Add(new AutoScrollBehavior());
BeginAddingItems();
}
private async void BeginAddingItems()
{
List<Task> tasks = new List<Task>();
await Task.Factory.StartNew(() =>
{
for (int i = 0; i < Int32.MaxValue; ++i)
{
AddToList("Added Slowly: " + i.ToString());
Thread.Sleep(2000);
if (i % 3 == 0)
{
for (int j = 0; j < 5; ++j)
{
AddToList("Added Quickly: " + j.ToString());
Thread.Sleep(200);
}
}
}
});
}
void AddToList(String item)
{
if (Application.Current == null)
return; // Application is shutting down.
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new Action(() => { data.Add(item); }));
}
private void clearButton_Click(object sender, RoutedEventArgs e)
{
data.Clear();
}
private void listbox_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
MessageBox.Show("Launch a modal dialog. Items are still added to the list in the background.");
}
}
}
MainWindow.xaml.cs:
<Window x:Class="BehaviorTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Test Scrolling"
FontFamily="Verdana"
Width="400" Height="250"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox x:Name="listbox" Grid.Row="0"
ItemsSource="{Binding data}"
MouseDoubleClick="listbox_MouseDoubleClick" >
</ListBox>
<StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right">
<Button x:Name="startButton" Click="clearButton_Click" MinWidth="80" >Clear</Button>
</StackPanel>
</Grid>
</Window>
You could try creating a Blend Behavior that does this for you. This is a small start:
public class AutoScrollBehavior:Behavior<ListBox>
{
private ScrollViewer scrollViewer;
private bool autoScroll = true;
protected override void OnAttached()
{
AssociatedObject.Loaded += AssociatedObjectOnLoaded;
AssociatedObject.Unloaded += AssociatedObjectOnUnloaded;
}
private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs routedEventArgs)
{
AssociatedObject.SelectionChanged -= AssociatedObjectOnSelectionChanged;
AssociatedObject.ItemContainerGenerator.ItemsChanged -= ItemContainerGeneratorItemsChanged;
scrollViewer = null;
}
private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
scrollViewer = GetScrollViewer(AssociatedObject);
if(scrollViewer != null)
{
scrollViewer.ScrollChanged += ScrollViewerOnScrollChanged;
AssociatedObject.SelectionChanged += AssociatedObjectOnSelectionChanged;
AssociatedObject.ItemContainerGenerator.ItemsChanged += ItemContainerGeneratorItemsChanged;
}
}
private void ScrollViewerOnScrollChanged(object sender, ScrollChangedEventArgs e) {
if (e.VerticalOffset == e.ExtentHeight-e.ViewportHeight) {
autoScroll = true;
}
}
private static ScrollViewer GetScrollViewer(DependencyObject root)
{
int childCount = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < childCount; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(root, i);
ScrollViewer sv = child as ScrollViewer;
if (sv != null)
return sv;
return GetScrollViewer(child);
}
return null;
}
private void ItemContainerGeneratorItemsChanged(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset) {
if (autoScroll) {
scrollViewer.ScrollToBottom();
}
}
}
private void AssociatedObjectOnSelectionChanged(object sender, SelectionChangedEventArgs selectionChangedEventArgs)
{
autoScroll = false;
}
}

Drag/Drop Button in same application throwing an exception

I have a Canvas that contains a button which I want to be able to drag and drop into another canvas. I want to copy the button to the other Canvas. Here is the code I am using:
The XAML:
<Window>
<Grid>
<Canvas
Height="300"
Width="500"
Background="Gray">
<Canvas
Name="cnvToolBox"
Canvas.Left="10"
Canvas.Top="10"
Background="AliceBlue"
Width="100"
Height="200">
<Button
Content="Drag Me!"
PreviewMouseLeftButtonDown="Button_PreviewMouseLeftButtonDown"
PreviewMouseMove="Button_PreviewMouseMove"></Button>
</Canvas>
<Rectangle
Canvas.Left="119"
Canvas.Top="9"
Width="102"
Height="202"
StrokeDashArray="0.5 1.0 0.3"
Stroke="Black"
StrokeThickness="2"/>
<Canvas
Name="cnvButtonDropZone"
Canvas.Left="120"
Canvas.Top="10"
Width="100"
Height="200"
Background="LightGreen"
AllowDrop="True"
DragEnter="Canvas_DragEnter"
Drop="Canvas_Drop">
</Canvas>
</Canvas>
</Grid>
</Window>
Here's the Code Behind:
public partial class MainWindow : Window
{
private Point startPoint;
public MainWindow()
{
InitializeComponent();
}
private void Button_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
startPoint = e.GetPosition(null);
}
private void Button_PreviewMouseMove(object sender, MouseEventArgs e)
{
Point currentPosition = e.GetPosition(null);
Vector diff = startPoint - currentPosition;
if (e.LeftButton == MouseButtonState.Pressed &&
(Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance))
{
Button button = sender as Button;
DataObject dragData = new DataObject("myFormat", button);
DragDrop.DoDragDrop(button, dragData, DragDropEffects.Copy);
}
}
private void Canvas_DragEnter(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent("myFormat") || sender == e.Source)
{
e.Effects = DragDropEffects.None;
}
}
private void Canvas_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent("myFormat"))
{
Button button = e.Data.GetData("myFormat") as Button;
Canvas canvas = sender as Canvas;
canvas.Children.Add(button);
}
}
}
When I drop the button I get the following exception when I'm adding the button to the canvas:
Specified element is already the logical child of another element. Disconnect it first.
I'm just trying to learn how to drag and drop controls and not really sure what that error means and how to resolve it. I don't know where I'm going wrong. Any suggestions would be welcome.
Thanks!
The button is owned by its parent cnvToolBox. You need to remove it from cnvToolBox before adding it to the canvas.
cnvToolBox.Children.Remove(button);
var canvas = sender as Canvas;
canvas.Children.Add(button);
This moves the button from your toolbox to the canvas. If you actually want to clone the item you want something like:
if (e.Data.GetDataPresent("myFormat"))
{
var contentControl = (ContentControl)e.Data.GetData("myFormat");
var constructorInfo = contentControl.GetType().GetConstructor(new Type[] {});
if (constructorInfo != null)
{
var newElement = (UIElement)constructorInfo.Invoke(new object[]{});
var newContentControl = newElement as ContentControl;
if(newContentControl != null)
{
newContentControl.Content = contentControl.Content;
}
((Panel)sender).Children.Add(newElement);
}
}
It's because the Button already has a parent associated with it; the previous Canvas.
You can set the parent of the Button to null; which will essentially remove it from the logical relationship.
button.Parent = null;
You will then be able to add that Button to another Canvas as you have done in your code behind.
You can also remove the Button from the Children property directly if you prefer and then add it accordingly within the new Canvas.
Canvas.Children.Remove(button);

Resources