Drawing diagram arcs with drag and drop in WPF - wpf

I'm trying to perform a drag and drop approach to creating relationships in a diagram, directly analagous to SQL Server Management Studio diagramming tools. For example, in the illustration below, the user would drag CustomerID from the User entity to the Customer entity and create a foreign key relationship between the two.
The key desired feature is that a temporary arc path would be drawn as the user performs the drag operation, following the mouse. Moving entities or relationships once created isn't the issue I'm running into.
Some reference XAML corresponding to an entity on the diagram above:
<!-- Entity diagram control -->
<Grid MinWidth="10" MinHeight="10" Margin="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*" ></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0" IsHitTestVisible="False" Background="{StaticResource ControlDarkBackgroundBrush}">
<Label Grid.Row="0" Grid.Column="0" Style="{DynamicResource LabelDiagram}" Content="{Binding DiagramHeader, Mode=OneWay}" />
</Grid>
<ScrollViewer Grid.Row="1" Grid.Column="0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Background="{StaticResource ControlBackgroundBrush}" >
<StackPanel VerticalAlignment="Top">
<uent:EntityDataPropertiesDiagramControl DataContext="{Binding EntityDataPropertiesFolder}" />
<uent:CollectionEntityPropertiesDiagramControl DataContext="{Binding CollectionEntityPropertiesFolder}" />
<uent:DerivedEntityDataPropertiesDiagramControl DataContext="{Binding DerivedEntityDataPropertiesFolder}" />
<uent:ReferenceEntityPropertiesDiagramControl DataContext="{Binding ReferenceEntityPropertiesFolder}" />
<uent:MethodsDiagramControl DataContext="{Binding MethodsFolder}" />
</StackPanel>
</ScrollViewer>
<Grid Grid.RowSpan="2" Margin="-10">
<lib:Connector x:Name="LeftConnector" Orientation="Left" VerticalAlignment="Center" HorizontalAlignment="Left" Visibility="Collapsed"/>
<lib:Connector x:Name="TopConnector" Orientation="Top" VerticalAlignment="Top" HorizontalAlignment="Center" Visibility="Collapsed"/>
<lib:Connector x:Name="RightConnector" Orientation="Right" VerticalAlignment="Center" HorizontalAlignment="Right" Visibility="Collapsed"/>
<lib:Connector x:Name="BottomConnector" Orientation="Bottom" VerticalAlignment="Bottom" HorizontalAlignment="Center" Visibility="Collapsed"/>
</Grid>
</Grid>
My current approach to doing this is to:
1) Initiate the drag operation in a child control of the entity, such as:
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
{
dragStartPoint = null;
}
else if (dragStartPoint.HasValue)
{
Point? currentPosition = new Point?(e.GetPosition(this));
if (currentPosition.HasValue && (Math.Abs(currentPosition.Value.X - dragStartPoint.Value.X) > 10 || Math.Abs(currentPosition.Value.Y - dragStartPoint.Value.Y) > 10))
{
DragDrop.DoDragDrop(this, DataContext, DragDropEffects.Link);
e.Handled = true;
}
}
}
2) Create a connector adorner when the drag operation leaves the entity, such as:
protected override void OnDragLeave(DragEventArgs e)
{
base.OnDragLeave(e);
if (ParentCanvas != null)
{
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(ParentCanvas);
if (adornerLayer != null)
{
ConnectorAdorner adorner = new ConnectorAdorner(ParentCanvas, BestConnector);
if (adorner != null)
{
adornerLayer.Add(adorner);
e.Handled = true;
}
}
}
}
3) Draw the arc path as the mouse is being moved in the connector adorner, such as:
protected override void OnMouseMove(MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (!IsMouseCaptured) CaptureMouse();
HitTesting(e.GetPosition(this));
pathGeometry = GetPathGeometry(e.GetPosition(this));
InvalidateVisual();
}
else
{
if (IsMouseCaptured) ReleaseMouseCapture();
}
}
The diagram Canvas is bound to a view model, and the entities and relationships on the Canvas are in turn bound to respective view models. Some XAML relating to the overall diagram:
<ItemsControl ItemsSource="{Binding Items, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<lib:DesignerCanvas VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
<Setter Property="Canvas.Width" Value="{Binding Width}"/>
<Setter Property="Canvas.Height" Value="{Binding Height}"/>
<Setter Property="Canvas.ZIndex" Value="{Binding ZIndex}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
and DataTemplates for the entites and relationships:
<!-- diagram relationship -->
<DataTemplate DataType="{x:Type dvm:DiagramRelationshipViewModel}">
<lib:Connection />
</DataTemplate>
<!-- diagram entity -->
<DataTemplate DataType="{x:Type dvm:DiagramEntityViewModel}">
<lib:DesignerItem>
<lib:EntityDiagramControl />
</lib:DesignerItem>
</DataTemplate>
Issue: The issue is that once the drag operation begins, mouse moves are no longer tracked and the connector adorner is unable to draw the arc as it does in other contexts. If I release the mouse and click again, then the arc starts drawing, but then I've lost my source object. I'm trying to figure a way to pass the source object in conjunction with mouse movement.
Bounty: Circling back to this issue, I currently plan to not use drag and drop directly to do this. I currently plan to add a DragItem and IsDragging DependencyProperty for the diagram control, which would hold the item being dragged, and flag if a drag operation is occuring. I could then use DataTriggers to change the Cursor and Adorner visibility based on IsDragging, and could use DragItem for the drop operation.
(But, I'm looking to award a bounty on another interesting approach. Please comment if more information or code is needed to clarify this question.)
Edit: Lower priority, but I'm still on the lookout for a better solution for a drag and drop diagramming approach. Want to implement a better approach in the open source Mo+ Solution Builder.

This is a fairly involved answer. Let me know if any part of it isn't clear.
I’m currently trying to solve a similar problem. In my case, I want to bind my ListBox ItemsSource to a collection and then represent every item in that collection as either a node i.e a draggable object or a connection i.e a line between nodes that redraws itself when the nodes are dragged. I’ll show you my code and detail where I think you might need to make changes to fit your needs.
Dragging
Dragging is accomplished by setting attached properties owned by the Dragger class. In my opinion, this has an advantage over using the MoveThumb to perform dragging in that making an object draggable does not involve changing its control template. My first implementation actually used MoveThumb in control templates to achieve dragging, but I found that doing so made my application very brittle (adding new features often broke the dragging). Here's the code for the Dragger:
public static class Dragger
{
private static FrameworkElement currentlyDraggedElement;
private static FrameworkElement CurrentlyDraggedElement
{
get { return currentlyDraggedElement; }
set
{
currentlyDraggedElement = value;
if (CurrentlyDraggedElement != null)
{
CurrentlyDraggedElement.MouseMove += new MouseEventHandler(CurrentlyDraggedElement_MouseMove);
CurrentlyDraggedElement.MouseLeftButtonUp +=new MouseButtonEventHandler(CurrentlyDraggedElement_MouseLeftButtonUp);
}
}
}
private static ItemPreviewAdorner adornerForDraggedItem;
private static ItemPreviewAdorner AdornerForDraggedItem
{
get { return adornerForDraggedItem; }
set { adornerForDraggedItem = value; }
}
#region IsDraggable
public static readonly DependencyProperty IsDraggableProperty = DependencyProperty.RegisterAttached("IsDraggable", typeof(Boolean), typeof(Dragger),
new FrameworkPropertyMetadata(IsDraggable_PropertyChanged));
public static void SetIsDraggable(DependencyObject element, Boolean value)
{
element.SetValue(IsDraggableProperty, value);
}
public static Boolean GetIsDraggable(DependencyObject element)
{
return (Boolean)element.GetValue(IsDraggableProperty);
}
#endregion
#region IsDraggingEvent
public static readonly RoutedEvent IsDraggingEvent = EventManager.RegisterRoutedEvent("IsDragging", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(Dragger));
public static event RoutedEventHandler IsDragging;
public static void AddIsDraggingHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
{
uie.AddHandler(Dragger.IsDraggingEvent, handler);
}
}
public static void RemoveIsDraggingEventHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
{
uie.RemoveHandler(Dragger.IsDraggingEvent, handler);
}
}
#endregion
public static void IsDraggable_PropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if ((bool)args.NewValue == true)
{
FrameworkElement element = (FrameworkElement)obj;
element.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(itemToBeDragged_MouseLeftButtonDown);
}
}
private static void itemToBeDragged_MouseLeftButtonDown(object sender, MouseEventArgs e)
{
var element = sender as FrameworkElement;
if (element != null)
{
CurrentlyDraggedElement = element;
}
}
private static void CurrentlyDraggedElement_MouseMove(object sender, MouseEventArgs e)
{
var element = sender as FrameworkElement;
if (element.IsEnabled == true)
{
element.CaptureMouse();
//RaiseIsDraggingEvent();
DragObject(sender, new Point(Mouse.GetPosition(PavilionVisualTreeHelper.GetAncestor(element, typeof(CustomCanvas)) as CustomCanvas).X,
Mouse.GetPosition(PavilionVisualTreeHelper.GetAncestor(element, typeof(CustomCanvas)) as CustomCanvas).Y));
}
}
private static void CurrentlyDraggedElement_MouseLeftButtonUp(object sender, MouseEventArgs e)
{
FrameworkElement element = sender as FrameworkElement;
element.MouseMove -= new MouseEventHandler(CurrentlyDraggedElement_MouseMove);
element.ReleaseMouseCapture();
CurrentlyDraggedElement = null;
}
private static void DragObject(object sender, Point startingPoint)
{
FrameworkElement item = sender as FrameworkElement;
if (item != null)
{
var canvas = PavilionVisualTreeHelper.GetAncestor(item, typeof(CustomCanvas)) as CustomCanvas;
double horizontalPosition = Mouse.GetPosition(canvas).X - item.ActualWidth/2;
double verticalPosition = Mouse.GetPosition(canvas).Y - item.ActualHeight/2;
item.RenderTransform = ReturnTransFormGroup(horizontalPosition, verticalPosition);
item.RaiseEvent(new IsDraggingRoutedEventArgs(item, new Point(horizontalPosition, verticalPosition), IsDraggingEvent));
}
}
private static TransformGroup ReturnTransFormGroup(double mouseX, double mouseY)
{
TransformGroup transformGroup = new TransformGroup();
transformGroup.Children.Add(new TranslateTransform(mouseX, mouseY));
return transformGroup;
}
}
public class IsDraggingRoutedEventArgs : RoutedEventArgs
{
public Point LocationDraggedTo { get; set;}
public FrameworkElement ElementBeingDragged { get; set; }
public IsDraggingRoutedEventArgs(DependencyObject elementBeingDragged, Point locationDraggedTo, RoutedEvent routedEvent)
: base(routedEvent)
{
this.ElementBeingDragged = elementBeingDragged as FrameworkElement;
LocationDraggedTo = locationDraggedTo;
}
}
I believe that Dragger requires that the object be on a Canvas or CustomCanvas, but there isn't any good reason, besides lazyness, for this. You could easily modify it to work for any Panel. (It’s in my backlog!).
The Dragger class is also using the PavilionVisualTreeHelper.GetAncestor() helper method, which simply climbs the Visual Tree looking for the appropriate element. The code for that is below.
/// <summary>
/// Gets ancestor of starting element
/// </summary>
/// <param name="parentType">Desired type of ancestor</param>
public static DependencyObject GetAncestor(DependencyObject startingElement, Type parentType)
{
if (startingElement == null || startingElement.GetType() == parentType)
return startingElement;
else
return GetAncestor(VisualTreeHelper.GetParent(startingElement), parentType);
}
Consuming the Dragger class is very simple. Simply set Dragger.IsDraggable = true in the appropriate control’s xaml markup. Optionally, you can register to the Dragger.IsDragging event, which bubbles up from the element being dragged, to perform any processing you might need.
Updating the Connection Position
My mechanism for informing the connection that it needs to be redrawn is a little sloppy, and definitely needs readdressing.
The Connection contains two DependencyProperties of type FrameworkElement: Start and End. In the PropertyChangedCallbacks, I try to cast them as DragAwareListBoxItems (I need to make this an interface for better reusability). If the cast is successful, I register to the DragAwareListBoxItem.ConnectionDragging event. (Bad name, not mine!). When that event fires, the connection redraws its path.
The DragAwareListBoxItem doesn’t actually know when it’s being dragged, so someone has to tell it. Because of the ListBoxItem’s position in my visual tree, it never hears the Dragger.IsDragging event. So to tell it that it’s being dragged, the ListBox listens to the event and and informs the appropriate DragAwareListBoxItem.
The was going to post the code for the Connection, the DragAwareListBoxItem, and the ListBox_IsDragging, but I think it's way too much to be readable here. You can check out the project at http://code.google.com/p/pavilion/source/browse/#hg%2FPavilionDesignerTool%2FPavilion.NodeDesigner
or clone the respository with hg clone https://code.google.com/p/pavilion/ . It's an open source project under the MIT license, so you can adapt it as you see fit. As a warning, there is no stable release, so it can change at any time.
Connectability
As with the Connection Updating, I won't paste the code. Instead, I'll tell you which classes in the project to examine and what to look for in each class.
From a user perspective, here's how creating a connection works. The user right-clicks on a node. This brings up a context menu from which the user selects "Create New Connection". That option creates a straight line whose starting point is rooted to the selected node, and whose end point follows the mouse. If the user clicks on another node, then a connection is created between the two. If the user clicks anywhere else, no connection is created and the line disappears.
Two classes are involved in this process. The ConnectionManager (which doesn't actually manage any connections) houses Attached Properties. The consuming control sets the ConnectionManager.IsConnectable property to true and sets the ConnectionManager.MenuItemInvoker property to the menu item that should start the process. Additionally, some control in your visual tree has to listen to the ConnectionPending routed event. This is where the actual creation of the connection takes place.
When the menu item is selected, the ConnectionManager creates a LineAdorner. The ConnectionManager listens to the LineAdorner LeftClick event. When that event is fired, I perform hit-testing to find the control that was selected. I then raise the ConnectionPending event, passing into the event args the two controls I want to create the connection between. It's up to the subscriber of the event to actually do the work.

I think you'll want to look into the WPF Thumb control. It wraps up some of this functionality in a convenient package.
Here's MSDN Documentation:
http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.thumb.aspx
Here's an example:
http://denisvuyka.wordpress.com/2007/10/13/wpf-draggable-objects-and-simple-shape-connectors/
Unfortunately I don't have a lot of experience in this area, but I do think that this is what you're looking for. Good luck!

As mentioned above, my current approach is to not use drag and drop directly, but to use a combination of DependencyProperties and handling mouse events to mimic a drag and drop.
The DependencyProperties in the parent diagram control are:
public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.Register("IsDragging", typeof(bool), typeof(SolutionDiagramControl));
public bool IsDragging
{
get
{
return (bool)GetValue(IsDraggingProperty);
}
set
{
SetValue(IsDraggingProperty, value);
}
}
public static readonly DependencyProperty DragItemProperty = DependencyProperty.Register("DragItem", typeof(IWorkspaceViewModel), typeof(SolutionDiagramControl));
public IWorkspaceViewModel DragItem
{
get
{
return (IWorkspaceViewModel)GetValue(DragItemProperty);
}
set
{
SetValue(DragItemProperty, value);
}
}
The IsDragging DependencyProperty is used to trigger a cursor change when a drag is taking place, such as:
<Style TargetType="{x:Type lib:SolutionDiagramControl}">
<Style.Triggers>
<Trigger Property="IsDragging" Value="True">
<Setter Property="Cursor" Value="Pen" />
</Trigger>
</Style.Triggers>
</Style>
Wherever I need to perform an arc drawing form of drag and drop, instead of calling DragDrop.DoDragDrop, I set IsDragging = true and DragItem to the source item being dragged.
Within the entity control on mouse leave, the connector adorner which draws the arc during the drag is enabled, such as:
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
if (ParentSolutionDiagramControl.DragItem != null)
{
CreateConnectorAdorner();
}
}
The diagram control must handle additional mouse events during the drag, such as:
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton != MouseButtonState.Pressed)
{
IsDragging = false;
DragItem = null;
}
}
The diagram control must also handle the "drop" upon a mouse up event (and it must figure out which entity is being dropped on based on mouse position), such as:
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
if (DragItem != null)
{
Point currentPosition = MouseUtilities.GetMousePosition(this);
DiagramEntityViewModel diagramEntityView = GetMouseOverEntity(currentPosition );
if (diagramEntityView != null)
{
// Perform the drop operations
}
}
IsDragging = false;
DragItem = null;
}
I am still looking for a better solution to draw the temporary arc (following the mouse) on the diagram while a drag operation is taking place.

Related

WPF MVVM: Strange Binding behavior

I have a UserControl that contains a TabControl.
<UserControl x:Class="Test.MyUC"
....
xmlns:vm="clr-namespace:Test.ViewModels"
xmlns:ikriv="clr-namespace:IKriv.Windows.Controls.Behaviors"
...
<UserControl.Resources>
<vm:MyUCVM x:Key="VM" />
</UserControl.Resources>
<UserControl.DataContext>
<StaticResourceExtension ResourceKey="VM" />
</UserControl.DataContext>
<!-- Using Ivan Krivyakov's Attached Behavior -->
<TabControl ikriv:TabContent.IsCached="True"
TabStripPlacement="Top" ItemsSource="{Binding TabList}" IsSynchronizedWithCurrentItem="True">
<TabControl.Resources>
<DataTemplate DataType="{x:Type vm:MyTab1VM}">
<v:MyTab1/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:MyTab2VM}">
<v:MyTab2/>
</DataTemplate>
</TabControl.Resources>
...
Of course, in MyUCVM, I have TabList. Now, up to this point, everything works fine.
The problem starts when one of the tabs (e.g. MyTab1) in the TabControl needs to continuously and recursively read data from some external source (done in the ViewModel of course), and pass that data to View (via Binding) to display. Even up to this point everything is working. However, I do not want that to run when the tab is not visible, because there is no point to do that.
To do that, MyTab1VM needs to know if the associated View (MyTab1) is the selected tab. Therefore, I wired this up:
MyTab1:
<Style TargetType="TabItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=OneWayToSource}" />
</Style>
MyTab1VM
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected",
typeof(bool),
typeof(MyTab1VM),
new PropertyMetadata(false, new PropertyChangedCallback(IsSelectedChanged))
);
public bool IsSelected
{
get
{
return (bool) GetValue(IsSelectedProperty);
}
set
{
SetValue(IsSelectedProperty, value);
}
}
public static void IsSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.Property == IsSelectedProperty)
{
MyTab1VM vm = d as MyTab1VM ;
vm.SetupToGetData();
}
}
private void SetupToGetData()
{
if (this.IsSelected)
{
System.Windows.Threading.DispatcherTimer timer = new System.Windows.Threading.DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(100);
timer.Tick += timer_Tick;
timer.Start();
}
}
private void timer_Tick(object sender, EventArgs e)
{
if (this.IsSelected)
this.MyData = ExternalSource.GetData();
else
{
(sender as System.Windows.Threading.DispatcherTimer).Stop();
}
}
Unfortunately, this setup only works when I set this.IsSelected = true; manually in the MyTab1VM's constructor. Leaving that out in the constructor, the data do not get shown in the view.
I have set breakpoints and confirmed that the binding for IsSelected is running correctly. Even the timer is running, and ExternalSource.GetData() is being called. But this.MyData = ExternalSource.GetData(); is not triggering the change from the ViewModel to the View.
The most puzzling part is that the same binding is triggered if IsSelected is set to true from the constructor.
Anyone out there knows what happened here?
I managed to do some fruitful troubleshooting on my own. I made a breakpoint in SetupToGetData() and I put this.GetHashCode() in my debugging watchlist. When I manually set this.IsSelected = true in the constructor, I realized that the SetupToGetData() method is called twice, with two different hash values. Planting another breakpoint in the constructor also showed that the constructor is called when I switch to this tab.
I have decided to move this to a new question, because it looks highly possible that the problem has nothing to do with binding.
Edit
Seems like I was right that this is the root of this problem. As that question is solved, so is this as well.

Scroll the scrollviewer to top through viewmodel

I am using the ScrollViewer with the MVVM pattern, and a list of items is wrapped by the ScrollViewer, such as
<ScrollViewer>
<ListView>
<ListView.View>
<GridView>
<GridViewColumn
Header = "Name"
DisplayMemberBinding="{Binding Path=Name}"
/>
</GridView>
</ListView.View>
</ListView>
</ScrollViewer>
The items of the listview are bound to a collection of objects in the viewmodel. I want the scrollviewer to scroll to the top whenever a item is added or removed from the collection.
I need the viewmodel to trigger the event, rather than using the ScrollToTop() method in the code-behind of the view.
IMHO, the clearest way to do this is using a "Behavior" via an AttachedProperty. An AttachedProperty is a mechanism to extend existing controls functionality.
First, create a class to hold the AtachedProperty, for instance:
public class ScrollViewerBehavior
{
public static bool GetAutoScrollToTop(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollToTopProperty);
}
public static void SetAutoScrollToTop(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollToTopProperty, value);
}
public static readonly DependencyProperty AutoScrollToTopProperty =
DependencyProperty.RegisterAttached("AutoScrollToTop", typeof(bool), typeof(ScrollViewerBehavior), new PropertyMetadata(false, (o, e) =>
{
var scrollViewer = o as ScrollViewer;
if (scrollViewer == null)
{
return;
}
if ((bool)e.NewValue)
{
scrollViewer.ScrollToTop();
SetAutoScrollToTop(o, false);
}
}));
}
This attached property allows a ScrollViewer having "magically" a new property of type Boolean, acting like a DependencyProperty in your XAML. If you bind this property to a standard property in your ViewModel, for instance:
private bool _reset;
public bool Reset
{
get { return _reset; }
set
{
_reset = value;
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Reset"));
}
}
(again, the name is up to you) and then you set this Reset property to true, your ScrollViewer will scroll to top.
I have named the AtachedPropertyas AutoScrollToTop, but the name is not important for this purpose.
The XAML will be something like:
<ScrollViewer my:ScrollViewerBehavior.AutoScrollToTop="{Binding Reset, Mode=TwoWay}">
<ListView>
<ListView.View>
<GridView>
<GridViewColumn
Header = "Name"
DisplayMemberBinding="{Binding Path=Name}"
/>
</GridView>
</ListView.View>
</ListView>
</ScrollViewer>
Note: my is the namespace where your ScrollViewerBehavior class lives. For example: xmlns:my="clr-namespace:MyApp.Behaviors"
Finally, the only thing you have to do in your ViewModel is to set Reset = true when you like, in your case, when you add or remove an element from the collection.
Create a new ListView control which extend Listview and use this new one instead
public class ScrollListView : ListView
{
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.OldItems.Count > 0)
this.ScrollIntoView(e.OldItems[e.OldStartingIndex]);
base.OnItemsChanged(e);
}
}
I have also faced a similar scenario where I needed to assign ScrollViewer's HorizontalOffset and VerticalOffset programmatically. I am afraid there is no direct binding mechanism for this. What I did was a way around (believe me, I still do not like the approach I followed, but I did not find any other option). Here is what I suggest:
Hook the ScrollViewer's Loaded event, cast the sender object to ScrollViewer and assign it to a property in DataContext (Means you need to keep a ScrollViewer propery in DataContext which will hold the reference of ScrollViewer in the UI). Hook up ObservableCollection's CollectionChanged events in ViewModel and using the ScrollViewer property, you can call methods like ScrollToTop() etc.
This is just a way around. I am still looking for better solution.
The simplest correct way to do this in MVVM is by creating an event in your viewmodel and subscribing to it from your view. And then, in the event handler, call ScrollToTop.
You fire the event from your viewmodel every time your collection is modified, for instance, and then it's up to the view to react to that event and scroll the list to the top.
Even if this involves some code-behind and demands that the view knows part of its viewmodel, it doesn't violate the MVVM pattern, unlike other workarounds.
public interface IMyViewModel
{
event EventHandler MyCollectionChanged;
}
public class MyViewModel : IMyViewModel
{
public event EventHandler MyCollectionChanged;
// More viewmodel related stuff
protected virtual void OnMyCollectionChanged(EventArgs e)
{
if (MyCollectionChanged != null)
MyCollectionChanged(this, e);
}
}
public class MyWindow : Window
{
public MyWindow(IMyViewModel viewModel)
{
this.DataContext = viewModel;
InitializeComponent();
(this.DataContext as IViewModel).MyCollectionChanged+= MyCollectionChangedEventHandler;
}
private void MyCollectionChangedEventHandler(object sender, EventArgs e)
{
// Do view related stuff
scrollViewer.ScrollToTop();
}
}
EDIT: But it can be refined a lot more, of course. If you want to avoid using code-behind, look for DataEventTriggers. If you don't mind about code-behind but are concerned about memory leaks, look for weak events.
And finally, since the logic you want is 100% view-related (have the ListView scroll every time an item is added or removed to it), you could also implement it as a Behavior / attached property, or extending the ListView. That could get a tad more convoluted, but I encourage you to give those options some thought.

Wpf ScrollViewer Scroll Amount

Is it possible to change the amount that the WPF ScrollViewer scrolls? I am simply wondering if it's possible to change the scrollviewer so that when using the mouse wheel or the scrollviewer arrows, the amount of incremental scrolling can be changed.
The short answer is: there is no way to do this without writing some custom scrolling code, but don't let that scare you it's not all that hard.
The ScrollViewer either works by scrolling using physical units (i.e. pixels) or by engaging with an IScrollInfo implementation to use logical units. This is controlled by the setting the CanContentScroll property where a value of false means "scroll the content using physical units" and a value of true means "scroll the content logically".
So how does the ScrollViewer scroll the content logically? By communicating with an IScrollInfo implementation. So that's how you could take over exactly how much the content of your panel scrolls when someone performs a logical action. Take a look at the documentation for IScrollInfo to get a listing of all the logical units of measurment that can be requested to scroll, but since you mentioned the mouse wheel you'll be mostly interested in the MouseWheelUp/Down/Left/Right methods.
Here's a simple, complete and working WPF ScrollViewer class that has a data-bindable SpeedFactor property for adjusting the mouse wheel sensitivity. Setting SpeedFactor to 1.0 means identical behavior to the WPF ScrollViewer. The default value for the dependency property is 2.5, which allows for very speedy wheel scrolling.
Of course, you can also create additional useful features by binding to the SpeedFactor property itself, i.e., to easily allow the user to control the multiplier.
public class WheelSpeedScrollViewer : ScrollViewer
{
public static readonly DependencyProperty SpeedFactorProperty =
DependencyProperty.Register(nameof(SpeedFactor),
typeof(Double),
typeof(WheelSpeedScrollViewer),
new PropertyMetadata(2.5));
public Double SpeedFactor
{
get { return (Double)GetValue(SpeedFactorProperty); }
set { SetValue(SpeedFactorProperty, value); }
}
protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
{
if (ScrollInfo is ScrollContentPresenter scp &&
ComputedVerticalScrollBarVisibility == Visibility.Visible)
{
scp.SetVerticalOffset(VerticalOffset - e.Delta * SpeedFactor);
e.Handled = true;
}
}
};
Complete XAML demo of 'fast mouse wheel scrolling' of around 3200 data items:
note: 'mscorlib' reference is only for accessing the demonstration data.
<UserControl x:Class="RemoveDuplicateTextLines.FastScrollDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyApp"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<local:WheelSpeedScrollViewer VerticalScrollBarVisibility="Auto">
<ListBox ItemsSource="{Binding Source={x:Type sys:Object},Path=Assembly.DefinedTypes}" />
</local:WheelSpeedScrollViewer>
</UserControl>
Fast mouse wheel:
You could implement a behavior on the scrollviewer. In my case CanContentScroll did not work. The solution below works for scrolling with the mouse wheel as well as draging the scrollbar.
public class StepSizeBehavior : Behavior<ScrollViewer>
{
public int StepSize { get; set; }
#region Attach & Detach
protected override void OnAttached()
{
CheckHeightModulesStepSize();
AssociatedObject.ScrollChanged += AssociatedObject_ScrollChanged;
base.OnAttached();
}
protected override void OnDetaching()
{
AssociatedObject.ScrollChanged -= AssociatedObject_ScrollChanged;
base.OnDetaching();
}
#endregion
[Conditional("DEBUG")]
private void CheckHeightModulesStepSize()
{
var height = AssociatedObject.Height;
var remainder = height%StepSize;
if (remainder > 0)
{
throw new ArgumentException($"{nameof(StepSize)} should be set to a value by which the height van be divised without a remainder.");
}
}
private void AssociatedObject_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
const double stepSize = 62;
var scrollViewer = (ScrollViewer)sender;
var steps = Math.Round(scrollViewer.VerticalOffset / stepSize, 0);
var scrollPosition = steps * stepSize;
if (scrollPosition >= scrollViewer.ScrollableHeight)
{
scrollViewer.ScrollToBottom();
return;
}
scrollViewer.ScrollToVerticalOffset(scrollPosition);
}
}
You would use it like this:
<ScrollViewer MaxHeight="248"
VerticalScrollBarVisibility="Auto">
<i:Interaction.Behaviors>
<behaviors:StepSizeBehavior StepSize="62" />
</i:Interaction.Behaviors>
I wanted to add to Drew Marsh accepted answer - while the other suggested answers solve it, in some cases you cannot override the PreviewMouseWheel event and handle it without causing other side effects. Namely if you have child controls that should receive priority to be scrolled before the parent ScrollViewer - like nested ListBox or ComboBox popups.
In my scenario, my parent control was a ItemsControl with its ItemsPanel being a VirtualizingStackPanel. I wanted its logical scrolling to be 1 unit per item instead of the default 3. Instead of fiddling with attached behaviors and intercepting/handling the mouse wheel events, I simply implemented a custom VirtualizingStackPanel to do this.
public class VirtualizingScrollSingleItemAtATimeStackPanel : VirtualizingStackPanel
{
public override void MouseWheelDown()
{
PageDown();
}
public override void MouseWheelUp()
{
PageUp();
}
public override void PageDown()
{
LineDown();
}
public override void PageUp()
{
LineUp();
}
}
then we use that panel like we normally would in our xaml markup:
<ItemsControl>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:VirtualizingScrollSingleItemAtATimeStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Obviously my scenario is contrived and the solution very simple, however this might provide a path for others to have better control over the scrolling behavior without the side effects I encountered.
I did this to ensure whole numbers on scrollbar1.ValueChanged:
scrollbar1.Value = Math.Round(scrollbar1.Value, 0, MidpointRounding.AwayFromZero)

Silverlight get selected row data in DataGrid

In my Silverlight application I have defined a datagrid with an template column containing a radio button as follows:
XAML:
<data:DataGrid x:Name="Grid1" Margin="8">
<data:DataGrid.Columns>
<data:DataGridTemplateColumn Header="RadioButtons">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<RadioButton x:Name="rdbIndataGrid" IsChecked="false" GroupName="myGroup" />
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
</data:DataGridTemplateColumn>
</data:DataGrid.Columns>
</data:DataGrid>
C#
public MainPage()
{
// Required to initialize variables
InitializeComponent();
string data = "1,2,3,4,5,6,7,8,9";
Grid1.ItemsSource = data.Split(',');
}
When a button is clicked I want to be able to:
a) Find out which radio button was selected.
b) Get the data from one of the cells in the grid which corresponds to the selected radio button.
Is there an easy way to do this? There doesnt seem to be a rows collection on the grid. Or do I have to bind it to a datasource and then check the data source?
Many thanks.
The way I'd prefer to do this would be to bind IsChecked to a property of objects assigned to the ItemsSource. But here I'll show you the hard way to do it
(Edit: Actually the following is over complicated for this scenario but I'll leave it here for now, see edits after)
First you need one of my VisualTreeEnumeration extension methods:-
public static class VisualTreeEnumeration
{
public static IEnumerable<DependencyObject> Ancestors(this DependencyObject root)
{
DependencyObject current = VisualTreeHelper.GetParent(root);
while (current != null)
{
yield return current;
current = VisualTreeHelper.GetParent(current);
}
}
}
Now in my testing I've just added a ListBox with the name lstOutput to my Xaml. Now add the following couple of event handlers to you UserControl :-
private void rdbIndataGrid_Checked(object sender, RoutedEventArgs e)
{
DataGridRow row = ((DependencyObject)sender).Ancestors().OfType<DataGridRow>().FirstOrDefault();
if (row != null)
lstOutput.Items.Add(String.Format("Checked: {0}", row.DataContext));
}
private void rdbIndataGrid_Unchecked(object sender, RoutedEventArgs e)
{
DataGridRow row = ((DependencyObject)sender).Ancestors().OfType<DataGridRow>().FirstOrDefault();
if (row != null)
lstOutput.Items.Add(String.Format("Unchecked: {0}", row.DataContext));
}
and finally tweak the Radio button Xaml like so:-
<RadioButton x:Name="rdbIndataGrid" IsChecked="false" GroupName="myGroup"
Checked="rdbIndataGrid_Checked" Unchecked="rdbIndataGrid_Unchecked" />
(One of the neat things about Xaml wiring up events is that it works even when the elements are part of a Template).
You'll note that in the event handlers I'm walking up the visual tree from the sending RadioButton to find the containing DataGridRow. The DataGridRow is the object that its DataContext set to the object being rendered by that row. In your own code you could cast the data context value to the correct type and from there access other data about the row.
Edit
Actually in most ordinary cases you don't need to hunt down the owning DataGridRow object accessing the DataContext property of the sending RadioButton is sufficient:-
private void rdbIndataGrid_Checked(object sender, RoutedEventArgs e)
{
object myData = ((FrameworkElement)sender).DataContext;
if (myData != null)
lstOutput.Items.Add(String.Format("Checked: {0}", myData));
}
private void rdbIndataGrid_Unchecked(object sender, RoutedEventArgs e)
{
object myData = ((FrameworkElement)sender).DataContext;
if (myData != null)
lstOutput.Items.Add(String.Format("Unchecked: {0}", myData));
}
Hence you can dispense with the Ancestors extension method. However in more complex cases where the DataContext have been changed the original "over-complicated" approach may be needed.

WPF M-V-VM: Get selected items from a ListCollectionView?

I've got a WPF app using the Model-View-ViewModel pattern.
In my ViewModel I've got a ListCollectionView to keep a list of items.
This ListCollectionView is bound to a ListBox in my View.
<ListBox Grid.Row="1" ItemsSource="{Binding Useragents}" SelectionMode="Multiple"/>
The ListBox has SelectionMode=Multiple, so you can select more items at one time. Now the ViewModel needs to know which items has been selected.
The problem is: in the View-Model-ViewModel pattern the ViewModel has no access to the View, so I can't just ask the ListBox which items has been selected. All I have is the ListCollectionView, but I can't find a way to find which items has been selected in there.
So how do I find which items has been selected in the ListBox? Or a trick to achieve this (maybe bind something to a Boolean 'IsSelected' in my items? But what? How?)
Maybe someone who is using this pattern, too, can help me here?
You need to create a ViewModel that has the concept of IsSelected on it and is bound to the IsSelected property of the actual ListBoxItem that represents it in the View using the standard WPF bindings architecture.
Then in your code, which knows about your ViewModel, but not the fact that it's represented by any specific View, can just use that property to find out which items from the Model are actually selected irrespective of the designers choice for how its represented in the View.
PRISM MVVM Reference Implementation has a behaviour called SynchronizeSelectedItems, used in Prism4\MVVM RI\MVVM.Client\Views\MultipleSelectionView.xaml, which synchronizes checked items with the ViewModel property named Selections:
<ListBox Grid.Column="0" Grid.Row="1" IsTabStop="False" SelectionMode="Multiple"
ItemsSource="{Binding Question.Range}" Margin="5">
<ListBox.ItemContainerStyle>
<!-- Custom style to show the multi-selection list box as a collection of check boxes -->
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid Background="Transparent">
<CheckBox IsChecked="{Binding IsSelected, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsHitTestVisible="False" IsTabStop="True"
AutomationProperties.AutomationId="CheckBoxAutomationId">
<ContentPresenter/>
</CheckBox>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<i:Interaction.Behaviors>
<!-- Custom behavior that synchronizes the selected items with the view models collection -->
<Behaviors:SynchronizeSelectedItems Selections="{Binding Selections}"/>
</i:Interaction.Behaviors>
</ListBox>
Go to http://compositewpf.codeplex.com/ and grab it all or use this:
//===================================================================================
// Microsoft patterns & practices
// Composite Application Guidance for Windows Presentation Foundation and Silverlight
//===================================================================================
// Copyright (c) Microsoft Corporation. All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===================================================================================
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious. No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
//===================================================================================
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace MVVM.Client.Infrastructure.Behaviors
{
/// <summary>
/// Custom behavior that synchronizes the list in <see cref="ListBox.SelectedItems"/> with a collection.
/// </summary>
/// <remarks>
/// This behavior uses a weak event handler to listen for changes on the synchronized collection.
/// </remarks>
public class SynchronizeSelectedItems : Behavior<ListBox>
{
public static readonly DependencyProperty SelectionsProperty =
DependencyProperty.Register(
"Selections",
typeof(IList),
typeof(SynchronizeSelectedItems),
new PropertyMetadata(null, OnSelectionsPropertyChanged));
private bool updating;
private WeakEventHandler<SynchronizeSelectedItems, object, NotifyCollectionChangedEventArgs> currentWeakHandler;
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly",
Justification = "Dependency property")]
public IList Selections
{
get { return (IList)this.GetValue(SelectionsProperty); }
set { this.SetValue(SelectionsProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.SelectionChanged += this.OnSelectedItemsChanged;
this.UpdateSelectedItems();
}
protected override void OnDetaching()
{
this.AssociatedObject.SelectionChanged += this.OnSelectedItemsChanged;
base.OnDetaching();
}
private static void OnSelectionsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var behavior = d as SynchronizeSelectedItems;
if (behavior != null)
{
if (behavior.currentWeakHandler != null)
{
behavior.currentWeakHandler.Detach();
behavior.currentWeakHandler = null;
}
if (e.NewValue != null)
{
var notifyCollectionChanged = e.NewValue as INotifyCollectionChanged;
if (notifyCollectionChanged != null)
{
behavior.currentWeakHandler =
new WeakEventHandler<SynchronizeSelectedItems, object, NotifyCollectionChangedEventArgs>(
behavior,
(instance, sender, args) => instance.OnSelectionsCollectionChanged(sender, args),
(listener) => notifyCollectionChanged.CollectionChanged -= listener.OnEvent);
notifyCollectionChanged.CollectionChanged += behavior.currentWeakHandler.OnEvent;
}
behavior.UpdateSelectedItems();
}
}
}
private void OnSelectedItemsChanged(object sender, SelectionChangedEventArgs e)
{
this.UpdateSelections(e);
}
private void UpdateSelections(SelectionChangedEventArgs e)
{
this.ExecuteIfNotUpdating(
() =>
{
if (this.Selections != null)
{
foreach (var item in e.AddedItems)
{
this.Selections.Add(item);
}
foreach (var item in e.RemovedItems)
{
this.Selections.Remove(item);
}
}
});
}
private void OnSelectionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
this.UpdateSelectedItems();
}
private void UpdateSelectedItems()
{
this.ExecuteIfNotUpdating(
() =>
{
if (this.AssociatedObject != null)
{
this.AssociatedObject.SelectedItems.Clear();
foreach (var item in this.Selections ?? new object[0])
{
this.AssociatedObject.SelectedItems.Add(item);
}
}
});
}
private void ExecuteIfNotUpdating(Action execute)
{
if (!this.updating)
{
try
{
this.updating = true;
execute();
}
finally
{
this.updating = false;
}
}
}
}
}
Look at this blogpost by Josh Smith The Initially Selected Item when Binding to a Grouped ICollectionView
The solution of Drew Marsh works very well, I recommend it. And I have another solution !
Model View ViewModel is a Passive View, you can also use a Presentation Model to access some datas of your presentation without being coupled with WPF
(this pattern is used in the Stocktrader example of PRISM).
Drew Marsh's answer is fine if you have a small list, if you have a large list the performance hit for finding all your selected items could be nasty!
My favorite solution is to create an attached property on your ListBox that then binds to an ObservableCollection which contains your selected items.
Then with your attached property you subscribe to the items SelectionChanged event to add/remove items from your collection.
For me the best answer is to break a little the principle of MVVM.
On the code behind
1. Instanciate your viewModel
2. add an event handler SelectionChanged
3. iterate through your selected items and add each item to your list of the viewModel
ViewModel viewModel = new ViewModel();
viewModel.SelectedModules = new ObservableCollection<string>();
foreach (var selectedModule in listBox1.SelectedItems)
{
viewModel.SelectedModules.Add(selectedModule.ToString());
}
Here is another variant of the View-Model-ViewModel Pattern where the ViewModel has access to the view through an IView interface.
I encountered quite a lot scenarios where you can't use WPF binding and then you need a way in code to synchronize the state between the View and the ViewModel.
How this can be done is shown here:
WPF Application Framework (WAF)
Have a look over here
http://blog.functionalfun.net/2009/02/how-to-databind-to-selecteditems.html
David Rogers' solution is great and is detailed at the below related question:
Sync SelectedItems in a muliselect listbox with a collection in ViewModel

Resources