I have a DragCanvas class that inherits from Canvas and that implements functionality in order to grab, drag and resize elements. The DragCanvas class is very similar to the one provided by Josh Smith in the following article: http://www.codeproject.com/Articles/15354/Dragging-Elements-in-a-Canvas
I'd like to be able to capture keyboard events as well in order to delete elements and duplicate them. I have overriden the OnKeydown and OnPreviewKeyDown methods and placed breakpoints in there, but they are never hitting. I'm quite inexperienced in WPF and I'm not sure what am I missing. Can you help me? Thanks in advance!
Here's the code for the drag canvas:
public class DragCanvas : Canvas
{
#region Data
// Stores a reference to the UIElement currently being dragged by the user.
private UIElement elementBeingDragged;
private UIElement elementBeingResized;
// Keeps track of where the mouse cursor was when a drag operation began.
private Point origCursorLocation;
// The offsets from the DragCanvas' edges when the drag operation began.
private double origHorizOffset, origVertOffset;
// Keeps track of which horizontal and vertical offset should be modified for the drag element.
private bool modifyLeftOffset, modifyTopOffset;
// True if a drag operation is underway, else false.
private bool isDragInProgress;
// True if a drag operation is underway and the mouse has moved since the process has started. This is used
// in order to determine on left mouse up whether we should display the resize adorners or not.
private bool hasMouseMovedInDragInProgress;
private AdornerLayer adornerLayer;
#endregion // Data
#region Attached Properties
#region CanBeDragged
public static readonly DependencyProperty CanBeDraggedProperty;
public static readonly DependencyProperty LineBelongsToBaseGridProperty;
public static bool GetCanBeDragged(UIElement uiElement)
{
if (uiElement == null)
return false;
return (bool)uiElement.GetValue(CanBeDraggedProperty);
}
public static void SetCanBeDragged(UIElement uiElement, bool value)
{
if (uiElement != null)
uiElement.SetValue(CanBeDraggedProperty, value);
}
#endregion // CanBeDragged
#endregion // Attached Properties
#region Dependency Properties
public static readonly DependencyProperty AllowDraggingProperty;
public static readonly DependencyProperty AllowDragOutOfViewProperty;
#endregion // Dependency Properties
#region Static Constructor
static DragCanvas()
{
AllowDraggingProperty = DependencyProperty.Register(
"AllowDragging",
typeof(bool),
typeof(DragCanvas),
new PropertyMetadata(true));
AllowDragOutOfViewProperty = DependencyProperty.Register(
"AllowDragOutOfView",
typeof(bool),
typeof(DragCanvas),
new UIPropertyMetadata(false));
CanBeDraggedProperty = DependencyProperty.RegisterAttached(
"CanBeDragged",
typeof(bool),
typeof(DragCanvas),
new UIPropertyMetadata(true));
LineBelongsToBaseGridProperty = DependencyProperty.RegisterAttached(
"LineBelongsToBaseGrid",
typeof(bool),
typeof(DragCanvas),
new UIPropertyMetadata(false));
}
#endregion // Static Constructor
#region Constructor
/// <summary>
/// Initializes a new instance of DragCanvas. UIElements in
/// the DragCanvas will immediately be draggable by the user.
/// </summary>
public DragCanvas()
{
}
#endregion // Constructor
#region Interface
#region AllowDragging
/// <summary>
/// Gets/sets whether elements in the DragCanvas should be draggable by the user.
/// The default value is true. This is a dependency property.
/// </summary>
public bool AllowDragging
{
get { return (bool)base.GetValue(AllowDraggingProperty); }
set { base.SetValue(AllowDraggingProperty, value); }
}
#endregion // AllowDragging
#region AllowDragOutOfView
/// <summary>
/// Gets/sets whether the user should be able to drag elements in the DragCanvas out of
/// the viewable area. The default value is false. This is a dependency property.
/// </summary>
public bool AllowDragOutOfView
{
get { return (bool)GetValue(AllowDragOutOfViewProperty); }
set { SetValue(AllowDragOutOfViewProperty, value); }
}
#endregion // AllowDragOutOfView
#region BringToFront / SendToBack
/// <summary>
/// Assigns the element a z-index which will ensure that
/// it is in front of every other element in the Canvas.
/// The z-index of every element whose z-index is between
/// the element's old and new z-index will have its z-index
/// decremented by one.
/// </summary>
/// <param name="targetElement">
/// The element to be sent to the front of the z-order.
/// </param>
public void BringToFront(UIElement element)
{
this.UpdateZOrder(element, true);
}
/// <summary>
/// Assigns the element a z-index which will ensure that
/// it is behind every other element in the Canvas.
/// The z-index of every element whose z-index is between
/// the element's old and new z-index will have its z-index
/// incremented by one.
/// </summary>
/// <param name="targetElement">
/// The element to be sent to the back of the z-order.
/// </param>
public void SendToBack(UIElement element)
{
this.UpdateZOrder(element, false);
}
#endregion // BringToFront / SendToBack
#region ElementBeingDragged
/// <summary>
/// Returns the UIElement currently being dragged, or null.
/// </summary>
/// <remarks>
/// Note to inheritors: This property exposes a protected
/// setter which should be used to modify the drag element.
/// </remarks>
public UIElement ElementBeingDragged
{
get
{
if (!this.AllowDragging)
return null;
else
return this.elementBeingDragged;
}
protected set
{
if (this.elementBeingDragged != null)
this.elementBeingDragged.ReleaseMouseCapture();
if (!this.AllowDragging)
this.elementBeingDragged = null;
else
{
if (DragCanvas.GetCanBeDragged(value))
{
this.elementBeingDragged = value;
this.elementBeingDragged.CaptureMouse();
}
else
this.elementBeingDragged = null;
}
}
}
#endregion // ElementBeingDragged
#region FindCanvasChild
/// <summary>
/// Walks up the visual tree starting with the specified DependencyObject,
/// looking for a UIElement which is a child of the Canvas. If a suitable
/// element is not found, null is returned. If the 'depObj' object is a
/// UIElement in the Canvas's Children collection, it will be returned.
/// </summary>
/// <param name="depObj">
/// A DependencyObject from which the search begins.
/// </param>
public UIElement FindCanvasChild(DependencyObject depObj)
{
while (depObj != null)
{
// If the current object is a UIElement which is a child of the
// Canvas, exit the loop and return it.
UIElement elem = depObj as UIElement;
if (elem != null && base.Children.Contains(elem))
break;
// VisualTreeHelper works with objects of type Visual or Visual3D.
// If the current object is not derived from Visual or Visual3D,
// then use the LogicalTreeHelper to find the parent element.
if (depObj is Visual || depObj is Visual3D)
depObj = VisualTreeHelper.GetParent(depObj);
else
depObj = LogicalTreeHelper.GetParent(depObj);
}
return depObj as UIElement;
}
#endregion // FindCanvasChild
#endregion // Interface
#region Overrides
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
int b;
b = 10;
return;
}
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
base.OnPreviewKeyDown(e);
int b;
b = 10;
return;
}
#region OnPreviewMouseLeftButtonDown
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnPreviewMouseLeftButtonDown(e);
this.isDragInProgress = false;
// If we have a mouse button down, check whether elementBeingResized is not null.
// If it's not, it means that we were resizing an element and we have now clicked somewhere else,
// so remove its resizing adorner.
if ((this.elementBeingResized != null) && (this.adornerLayer != null))
{
var adorners = adornerLayer.GetAdorners(elementBeingResized);
if (adorners != null)
adornerLayer.Remove(adorners[0]);
}
// Cache the mouse cursor location.
this.origCursorLocation = e.GetPosition(this);
// Walk up the visual tree from the element that was clicked,
// looking for an element that is a direct child of the Canvas.
this.elementBeingResized = this.ElementBeingDragged = this.FindCanvasChild(e.Source as DependencyObject);
if (this.ElementBeingDragged == null)
return;
if ((bool)ElementBeingDragged.GetValue(LineBelongsToBaseGridProperty))
{
this.ElementBeingDragged = null;
return;
}
// Get the element's offsets from the four sides of the Canvas.
double left = Canvas.GetLeft(this.ElementBeingDragged);
double right = Canvas.GetRight(this.ElementBeingDragged);
double top = Canvas.GetTop(this.ElementBeingDragged);
double bottom = Canvas.GetBottom(this.ElementBeingDragged);
// Calculate the offset deltas and determine for which sides
// of the Canvas to adjust the offsets.
this.origHorizOffset = ResolveOffset(left, right, out this.modifyLeftOffset);
this.origVertOffset = ResolveOffset(top, bottom, out this.modifyTopOffset);
// Set the Handled flag so that a control being dragged
// does not react to the mouse input.
e.Handled = true;
this.isDragInProgress = true;
}
#endregion // OnPreviewMouseLeftButtonDown
protected override void OnPreviewMouseRightButtonDown(MouseButtonEventArgs e)
{
base.OnPreviewMouseRightButtonDown(e);
}
#endregion
#region OnPreviewMouseMove
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
base.OnPreviewMouseMove(e);
// If no element is being dragged, there is nothing to do.
if (this.ElementBeingDragged == null || !this.isDragInProgress)
return;
hasMouseMovedInDragInProgress = true;
// Get the position of the mouse cursor, relative to the Canvas.
Point cursorLocation = e.GetPosition(this);
// These values will store the new offsets of the drag element.
double newHorizontalOffset, newVerticalOffset;
#region Calculate Offsets
// Determine the horizontal offset.
if (this.modifyLeftOffset)
newHorizontalOffset = this.origHorizOffset + (cursorLocation.X - this.origCursorLocation.X);
else
newHorizontalOffset = this.origHorizOffset - (cursorLocation.X - this.origCursorLocation.X);
// Determine the vertical offset.
if (this.modifyTopOffset)
newVerticalOffset = this.origVertOffset + (cursorLocation.Y - this.origCursorLocation.Y);
else
newVerticalOffset = this.origVertOffset - (cursorLocation.Y - this.origCursorLocation.Y);
#endregion // Calculate Offsets
if (!this.AllowDragOutOfView)
{
#region Verify Drag Element Location
// Get the bounding rect of the drag element.
Rect elemRect = this.CalculateDragElementRect(newHorizontalOffset, newVerticalOffset);
//
// If the element is being dragged out of the viewable area,
// determine the ideal rect location, so that the element is
// within the edge(s) of the canvas.
//
bool leftAlign = elemRect.Left < 0;
bool rightAlign = elemRect.Right > this.ActualWidth;
if (leftAlign)
newHorizontalOffset = modifyLeftOffset ? 0 : this.ActualWidth - elemRect.Width;
else if (rightAlign)
newHorizontalOffset = modifyLeftOffset ? this.ActualWidth - elemRect.Width : 0;
bool topAlign = elemRect.Top < 0;
bool bottomAlign = elemRect.Bottom > this.ActualHeight;
if (topAlign)
newVerticalOffset = modifyTopOffset ? 0 : this.ActualHeight - elemRect.Height;
else if (bottomAlign)
newVerticalOffset = modifyTopOffset ? this.ActualHeight - elemRect.Height : 0;
#endregion // Verify Drag Element Location
}
#region Move Drag Element
if (this.modifyLeftOffset)
Canvas.SetLeft(this.ElementBeingDragged, newHorizontalOffset);
else
Canvas.SetRight(this.ElementBeingDragged, newHorizontalOffset);
if (this.modifyTopOffset)
Canvas.SetTop(this.ElementBeingDragged, newVerticalOffset);
else
Canvas.SetBottom(this.ElementBeingDragged, newVerticalOffset);
#endregion // Move Drag Element
}
#endregion // OnPreviewMouseMove
#region OnHostPreviewMouseUp
protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
{
base.OnPreviewMouseUp(e);
if ((elementBeingResized != null) && !hasMouseMovedInDragInProgress)
{
// If no call to MouseMove has been issues during the drag process, it means that the user wants to resize it.
adornerLayer = AdornerLayer.GetAdornerLayer(elementBeingResized);
adornerLayer.Add(new ResizingAdorner(elementBeingResized));
}
hasMouseMovedInDragInProgress = false;
// Reset the field whether the left or right mouse button was
// released, in case a context menu was opened on the drag element.
this.ElementBeingDragged = null;
}
#endregion // OnHostPreviewMouseUp
#region HostEventHandlers
#endregion // Host Event Handlers
#region Private Helpers
#region CalculateDragElementRect
/// <summary>
/// Returns a Rect which describes the bounds of the element being dragged.
/// </summary>
private Rect CalculateDragElementRect(double newHorizOffset, double newVertOffset)
{
if (this.ElementBeingDragged == null)
throw new InvalidOperationException("ElementBeingDragged is null.");
Size elemSize = this.ElementBeingDragged.RenderSize;
double x, y;
if (this.modifyLeftOffset)
x = newHorizOffset;
else
x = this.ActualWidth - newHorizOffset - elemSize.Width;
if (this.modifyTopOffset)
y = newVertOffset;
else
y = this.ActualHeight - newVertOffset - elemSize.Height;
Point elemLoc = new Point(x, y);
return new Rect(elemLoc, elemSize);
}
#endregion // CalculateDragElementRect
#region ResolveOffset
/// <summary>
/// Determines one component of a UIElement's location
/// within a Canvas (either the horizontal or vertical offset).
/// </summary>
/// <param name="side1">
/// The value of an offset relative to a default side of the
/// Canvas (i.e. top or left).
/// </param>
/// <param name="side2">
/// The value of the offset relative to the other side of the
/// Canvas (i.e. bottom or right).
/// </param>
/// <param name="useSide1">
/// Will be set to true if the returned value should be used
/// for the offset from the side represented by the 'side1'
/// parameter. Otherwise, it will be set to false.
/// </param>
private static double ResolveOffset(double side1, double side2, out bool useSide1)
{
// If the Canvas.Left and Canvas.Right attached properties
// are specified for an element, the 'Left' value is honored.
// The 'Top' value is honored if both Canvas.Top and
// Canvas.Bottom are set on the same element. If one
// of those attached properties is not set on an element,
// the default value is Double.NaN.
useSide1 = true;
double result;
if (Double.IsNaN(side1))
{
if (Double.IsNaN(side2))
{
// Both sides have no value, so set the
// first side to a value of zero.
result = 0;
}
else
{
result = side2;
useSide1 = false;
}
}
else
{
result = side1;
}
return result;
}
#endregion // ResolveOffset
#region UpdateZOrder
/// <summary>
/// Helper method used by the BringToFront and SendToBack methods.
/// </summary>
/// <param name="element">
/// The element to bring to the front or send to the back.
/// </param>
/// <param name="bringToFront">
/// Pass true if calling from BringToFront, else false.
/// </param>
private void UpdateZOrder(UIElement element, bool bringToFront)
{
#region Safety Check
if (element == null)
throw new ArgumentNullException("element");
if (!base.Children.Contains(element))
throw new ArgumentException("Must be a child element of the Canvas.", "element");
#endregion // Safety Check
#region Calculate Z-Indici And Offset
// Determine the Z-Index for the target UIElement.
int elementNewZIndex = -1;
if (bringToFront)
{
foreach (UIElement elem in base.Children)
if (elem.Visibility != Visibility.Collapsed)
++elementNewZIndex;
}
else
{
elementNewZIndex = 0;
}
// Determine if the other UIElements' Z-Index
// should be raised or lowered by one.
int offset = (elementNewZIndex == 0) ? +1 : -1;
int elementCurrentZIndex = Canvas.GetZIndex(element);
#endregion // Calculate Z-Indici And Offset
#region Update Z-Indici
// Update the Z-Index of every UIElement in the Canvas.
foreach (UIElement childElement in base.Children)
{
if (childElement == element)
Canvas.SetZIndex(element, elementNewZIndex);
else
{
int zIndex = Canvas.GetZIndex(childElement);
// Only modify the z-index of an element if it is
// in between the target element's old and new z-index.
if (bringToFront && elementCurrentZIndex < zIndex ||
!bringToFront && zIndex < elementCurrentZIndex)
{
Canvas.SetZIndex(childElement, zIndex + offset);
}
}
}
#endregion // Update Z-Indici
}
#endregion // UpdateZOrder
#endregion // Private Helpers
}
Here's the snippet in the MainWindow.xaml that creates the drag canvas:
<DragCanvas:DragCanvas x:Name="editCanvas" Margin="196,27,9,236" Background="Aquamarine" Focusable="True"/>
And here's the constructor code in the code-behind file:
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
Initialize();
Loaded += (x, y) => Keyboard.Focus(editCanvas);
}
DragHelper _dragHelper;
DropHelper _dropHelper;
private List<Line> _gridLines = new List<Line>();
internal void Initialize()
{
var callback = new ListBoxDragDropDataProvider(this.listSrc);
_dragHelper = new DragHelper(this.listSrc, callback, null);
_dropHelper = new DropHelper(this.editCanvas);
MainWindowViewModel.SetMainWindowView(this);
}
public MainWindowViewModel MainWindowViewModel
{
get { return DataContext as MainWindowViewModel; }
}
Set Focusable="True" for your canvas, then put this snippet into your window class and set DragCanvas into focus. Hope this helps you.
Loaded += (x,y) => Keyboard.Focus(DragCanvas);
Can you please check by setting Focusable="True" for canvas. Hope it works.
Related
In iOS when you are typing a password into a field the last letter of the field is displayed but then is obfuscated when you type the next character. Is there a way to duplicate this behavior in WPF?
If your usage for such a thing in a desktop app is justified, then you can do something like the following.
We had a similar requirement before and this is what I did.
I created a custom Passwordbox by deriving from TextBox and adding a new DP of type SecureString to it (pretty much the same concept as a normal PasswordBox). We do not lose any security benefits this way and can customize the visual behavior to our heart's content.
With this now we can use the Text of the TextBox as it's display-string and hold the actual password in the back-end SecureString DP and bind it to the VM.
We handle the PreviewTextInput and PreviewKeyDown events to manage all text changes in the control, including stuff like Key.Back, Key.Delete and the annoying Key.Space(which does not come through the PreviewTextInput
iOS Feel:
Couple more things to note for an exact iOS behavior.
Last character is only shown while adding new characters to the "end of the current string" (FlowDirection independent)
Editing characters in-between an existing string has no effect on mask.
Last character shown is timer-dependent (becomes an "*" after a certain period if left idle)
All copy-paste operations disabled in the control.
First 2 points can be handled pretty easily when detecting text changes, for the last one we can use a DispatcherTimer to work with the display-string accordingly.
So putting this all together we end up with:
/// <summary>
/// This class contains properties for CustomPasswordBox
/// </summary>
internal class CustomPasswordBox : TextBox {
#region Member Variables
/// <summary>
/// Dependency property to hold watermark for CustomPasswordBox
/// </summary>
public static readonly DependencyProperty PasswordProperty =
DependencyProperty.Register(
"Password", typeof(SecureString), typeof(CustomPasswordBox), new UIPropertyMetadata(new SecureString()));
/// <summary>
/// Private member holding mask visibile timer
/// </summary>
private readonly DispatcherTimer _maskTimer;
#endregion
#region Constructors
/// <summary>
/// Initialises a new instance of the LifeStuffPasswordBox class.
/// </summary>
public CustomPasswordBox() {
PreviewTextInput += OnPreviewTextInput;
PreviewKeyDown += OnPreviewKeyDown;
CommandManager.AddPreviewExecutedHandler(this, PreviewExecutedHandler);
_maskTimer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 1) };
_maskTimer.Tick += (sender, args) => MaskAllDisplayText();
}
#endregion
#region Commands & Properties
/// <summary>
/// Gets or sets dependency Property implementation for Password
/// </summary>
public SecureString Password {
get {
return (SecureString)GetValue(PasswordProperty);
}
set {
SetValue(PasswordProperty, value);
}
}
#endregion
#region Methods
/// <summary>
/// Method to handle PreviewExecutedHandler events
/// </summary>
/// <param name="sender">Sender object</param>
/// <param name="executedRoutedEventArgs">Event Text Arguments</param>
private static void PreviewExecutedHandler(object sender, ExecutedRoutedEventArgs executedRoutedEventArgs) {
if (executedRoutedEventArgs.Command == ApplicationCommands.Copy ||
executedRoutedEventArgs.Command == ApplicationCommands.Cut ||
executedRoutedEventArgs.Command == ApplicationCommands.Paste) {
executedRoutedEventArgs.Handled = true;
}
}
/// <summary>
/// Method to handle PreviewTextInput events
/// </summary>
/// <param name="sender">Sender object</param>
/// <param name="textCompositionEventArgs">Event Text Arguments</param>
private void OnPreviewTextInput(object sender, TextCompositionEventArgs textCompositionEventArgs) {
AddToSecureString(textCompositionEventArgs.Text);
textCompositionEventArgs.Handled = true;
}
/// <summary>
/// Method to handle PreviewKeyDown events
/// </summary>
/// <param name="sender">Sender object</param>
/// <param name="keyEventArgs">Event Text Arguments</param>
private void OnPreviewKeyDown(object sender, KeyEventArgs keyEventArgs) {
Key pressedKey = keyEventArgs.Key == Key.System ? keyEventArgs.SystemKey : keyEventArgs.Key;
switch (pressedKey) {
case Key.Space:
AddToSecureString(" ");
keyEventArgs.Handled = true;
break;
case Key.Back:
case Key.Delete:
if (SelectionLength > 0) {
RemoveFromSecureString(SelectionStart, SelectionLength);
} else if (pressedKey == Key.Delete && CaretIndex < Text.Length) {
RemoveFromSecureString(CaretIndex, 1);
} else if (pressedKey == Key.Back && CaretIndex > 0) {
int caretIndex = CaretIndex;
if (CaretIndex > 0 && CaretIndex < Text.Length)
caretIndex = caretIndex - 1;
RemoveFromSecureString(CaretIndex - 1, 1);
CaretIndex = caretIndex;
}
keyEventArgs.Handled = true;
break;
}
}
/// <summary>
/// Method to add new text into SecureString and process visual output
/// </summary>
/// <param name="text">Text to be added</param>
private void AddToSecureString(string text) {
if (SelectionLength > 0) {
RemoveFromSecureString(SelectionStart, SelectionLength);
}
foreach (char c in text) {
int caretIndex = CaretIndex;
Password.InsertAt(caretIndex, c);
MaskAllDisplayText();
if (caretIndex == Text.Length) {
_maskTimer.Stop();
_maskTimer.Start();
Text = Text.Insert(caretIndex++, c.ToString());
} else {
Text = Text.Insert(caretIndex++, "*");
}
CaretIndex = caretIndex;
}
}
/// <summary>
/// Method to remove text from SecureString and process visual output
/// </summary>
/// <param name="startIndex">Start Position for Remove</param>
/// <param name="trimLength">Length of Text to be removed</param>
private void RemoveFromSecureString(int startIndex, int trimLength) {
int caretIndex = CaretIndex;
for (int i = 0; i < trimLength; ++i) {
Password.RemoveAt(startIndex);
}
Text = Text.Remove(startIndex, trimLength);
CaretIndex = caretIndex;
}
private void MaskAllDisplayText() {
_maskTimer.Stop();
int caretIndex = CaretIndex;
Text = new string('*', Text.Length);
CaretIndex = caretIndex;
}
#endregion
}
Working Sample:
Download Link
You can type something into the control and check the stored value shown below it.
In this sample, I've added a new DP of type string to just show that the control works fine. You'd obviously not want to have that DP (HiddenText) in your live-code, but I'd hope the sample helps to analyze if the class actually works :)
I have some problems with my data grid. My project is transforming a Delphi project to .Net. The product owner want the same behaviour for the datagrids.
When positioned on the last cell and tab or enter is hit, the following should happen:
A new row is added
The first cell in the new row is selected
Other demands for the datagrid is:
The focus should remain inside the datagrid once it has the focus (ALT + key combinations is the way to leave the datagrid again).
The datagrid is databound
The datagrid is used in MVVM
We use the .net4.0 full profile
This article had the best solution I could find.
I preferred to use an attached property rather than a behavior, since this enabled me to set it easily in the default style for DataGrid. Here's the code:
namespace SampleDataGridApp
{
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
/// <summary>
/// An attached behavior that modifies the tab behavior for a <see cref="DataGrid"/>.
/// </summary>
public static class DataGridBehavior
{
/// <summary>
/// Identifies the <c>NewLineOnTab</c> attached property.
/// </summary>
public static readonly DependencyProperty NewLineOnTabProperty = DependencyProperty.RegisterAttached(
"NewLineOnTab",
typeof(bool),
typeof(DataGridBehavior),
new PropertyMetadata(default(bool), OnNewLineOnTabChanged));
/// <summary>
/// Sets the value of the <c>NewLineOnTab</c> attached property.
/// </summary>
/// <param name="element">The <see cref="DataGrid"/>.</param>
/// <param name="value">A value indicating whether to apply the behavior.</param>
public static void SetNewLineOnTab(DataGrid element, bool value)
{
element.SetValue(NewLineOnTabProperty, value);
}
/// <summary>
/// Gets the value of the <c>NewLineOnTab</c> attached property.
/// </summary>
/// <param name="element">The <see cref="DataGrid"/>.</param>
/// <returns>A value indicating whether to apply the behavior.</returns>
public static bool GetNewLineOnTab(DataGrid element)
{
return (bool)element.GetValue(NewLineOnTabProperty);
}
/// <summary>
/// Called when the value of the <c>NewLineOnTab</c> property changes.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private static void OnNewLineOnTabChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
DataGrid d = sender as DataGrid;
if (d == null)
{
return;
}
bool newValue = (bool)e.NewValue;
bool oldValue = (bool)e.OldValue;
if (oldValue == newValue)
{
return;
}
if (oldValue)
{
d.PreviewKeyDown -= AssociatedObjectKeyDown;
}
else
{
d.PreviewKeyDown += AssociatedObjectKeyDown;
KeyboardNavigation.SetTabNavigation(d, KeyboardNavigationMode.Contained);
}
}
/// <summary>
/// Handles the <see cref="UIElement.KeyDown"/> event for a <see cref="DataGridCell"/>.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private static void AssociatedObjectKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Tab)
{
return;
}
DataGrid dg = e.Source as DataGrid;
if (dg == null)
{
return;
}
if (dg.CurrentColumn.DisplayIndex == dg.Columns.Count - 1)
{
var icg = dg.ItemContainerGenerator;
if (dg.SelectedIndex == icg.Items.Count - 2)
{
dg.CommitEdit(DataGridEditingUnit.Row, false);
}
}
}
}
}
My default style looks like this:
<Style TargetType="DataGrid">
<Setter Property="GridLinesVisibility" Value="None" />
<Setter Property="KeyboardNavigation.TabNavigation" Value="Contained" />
<Setter Property="sampleDataGridApp:DataGridBehavior.NewLineOnTab" Value="True" />
<Setter Property="IsSynchronizedWithCurrentItem" Value="True" />
</Style>
If the last column's DataGridCell has it's IsTabStop set to false like in this example the above will not work.
Here is a buggy workaround:
private static void AssociatedObjectKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Tab)
{
return;
}
DataGrid dg = e.Source as DataGrid;
if (dg == null)
{
return;
}
int offSet = 1;
var columnsReversed = dg.Columns.Reverse();
foreach (var dataGridColumn in columnsReversed)
{
// Bug: This makes the grand assumption that a readonly column's "DataGridCell" has IsTabStop == false;
if (dataGridColumn.IsReadOnly)
{
offSet++;
}
else
{
break;
}
}
if (dg.CurrentColumn.DisplayIndex == (dg.Columns.Count - offSet))
{
var icg = dg.ItemContainerGenerator;
if (dg.SelectedIndex == icg.Items.Count - 2)
{
dg.CommitEdit(DataGridEditingUnit.Row, false);
}
}
}
Ok, I have been fighting this problem for many hours now. I have tried nearly every proposed solution out there and here is what I found that works for me...
private void grid_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Tab)
{
if (grid.SelectedIndex == grid.Items.Count - 2 && grid.CurrentColumn.DisplayIndex == grid.Columns.Count - 1)
{
grid.CommitEdit(DataGridEditingUnit.Row, false);
e.Handled = true;
}
}
}
private void DataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
if (grid.SelectedIndex == grid.Items.Count - 2)
{
grid.SelectedIndex = grid.Items.Count - 1;
grid.CurrentCell = new DataGridCellInfo(grid.Items[grid.Items.Count - 1], grid.Columns[0]);
}
}
What this code does is when you tab to the last cell of the last row and press tab it will move focus to the first cell of the new row. Which is what you would expect but this is not default behavior. The default behavior is to move focus to the next control and not commit the current row edit. This is clearly a bug in the DataGrid I believe which is why all the proposed solutions have a whiff of kluge. My solution doesn't smell that good I admit, but if you agree that this is bug, I prefer this to the ridiculous default behavior.
This solution works even if the grid is sorted. The newly entered row will sort to the proper place but the focus will be put on the first column of the new row.
The only unsolved issue is that when tabbing down from the top to the last cell before the new row, tab must be entered twice before focus is moved to the new row. I looked into this quirk for a bit and finally gave up on it.
When navigating between Views/ViewModels using RequestNavigate (i.e. programmatically), the IConfirmNavigationRequest methods on the appropriate ViewModels are called as expected. However, if you switch views in a TabControl region by clicking on the tab, it does not call those methods.
Is this the expected and accepted behaviour? Would I be able to implement a prism behavior to make this work?
Any advice would be appreciated.
UPDATE
I've decided to explain the problem more thoroughly based on Viktor's feedback. I want to prevent navigation if the user has unsaved edits on the screen. Switching tabs IMHO is just another way to navigate. I expect the Prism implementation to be consistent: navigating programmatically or otherwise should have the same behaviour.
If I were to create an ItemsControl with buttons that when clicked navigates by using RequestNavigate (to effectively switch tabs) it would work, but that isn't the point of the question.
I suppose I can see your point, and I understand why you would like it to call the RequestNavigate method.
To answer your question, yes this is by design and it is not supposed to call RequestNavigate while switching tabs. However, you can modify this behavior to do what you want. Prism is open source. You should have the source code, you can add the project to your project and easily step through the code for the following:
TabControlRegionAdapter - Adapts the region to the tab control
public class TabControlRegionAdapter : RegionAdapterBase<TabControl>
{
/// <summary>
/// <see cref="Style"/> to set to the created <see cref="TabItem"/>.
/// </summary>
public static readonly DependencyProperty ItemContainerStyleProperty =
DependencyProperty.RegisterAttached("ItemContainerStyle", typeof(Style), typeof(TabControlRegionAdapter), null);
/// <summary>
/// Initializes a new instance of the <see cref="TabControlRegionAdapter"/> class.
/// </summary>
/// <param name="regionBehaviorFactory">The factory used to create the region behaviors to attach to the created regions.</param>
public TabControlRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory)
: base(regionBehaviorFactory)
{
}
/// <summary>
/// Gets the <see cref="ItemContainerStyleProperty"/> property value.
/// </summary>
/// <param name="target">Target object of the attached property.</param>
/// <returns>Value of the <see cref="ItemContainerStyleProperty"/> property.</returns>
public static Style GetItemContainerStyle(DependencyObject target)
{
if (target == null) throw new ArgumentNullException("target");
return (Style)target.GetValue(ItemContainerStyleProperty);
}
/// <summary>
/// Sets the <see cref="ItemContainerStyleProperty"/> property value.
/// </summary>
/// <param name="target">Target object of the attached property.</param>
/// <param name="value">Value to be set on the <see cref="ItemContainerStyleProperty"/> property.</param>
public static void SetItemContainerStyle(DependencyObject target, Style value)
{
if (target == null) throw new ArgumentNullException("target");
target.SetValue(ItemContainerStyleProperty, value);
}
/// <summary>
/// Adapts a <see cref="TabControl"/> to an <see cref="IRegion"/>.
/// </summary>
/// <param name="region">The new region being used.</param>
/// <param name="regionTarget">The object to adapt.</param>
protected override void Adapt(IRegion region, TabControl regionTarget)
{
if (regionTarget == null) throw new ArgumentNullException("regionTarget");
bool itemsSourceIsSet = regionTarget.ItemsSource != null;
if (itemsSourceIsSet)
{
throw new InvalidOperationException(Resources.ItemsControlHasItemsSourceException);
}
}
/// <summary>
/// Attach new behaviors.
/// </summary>
/// <param name="region">The region being used.</param>
/// <param name="regionTarget">The object to adapt.</param>
/// <remarks>
/// This class attaches the base behaviors and also keeps the <see cref="TabControl.SelectedItem"/>
/// and the <see cref="IRegion.ActiveViews"/> in sync.
/// </remarks>
protected override void AttachBehaviors(IRegion region, TabControl regionTarget)
{
if (region == null) throw new ArgumentNullException("region");
base.AttachBehaviors(region, regionTarget);
if (!region.Behaviors.ContainsKey(TabControlRegionSyncBehavior.BehaviorKey))
{
region.Behaviors.Add(TabControlRegionSyncBehavior.BehaviorKey, new TabControlRegionSyncBehavior { HostControl = regionTarget });
}
}
/// <summary>
/// Creates a new instance of <see cref="Region"/>.
/// </summary>
/// <returns>A new instance of <see cref="Region"/>.</returns>
protected override IRegion CreateRegion()
{
return new SingleActiveRegion();
}
}
And also, TabControlRegionSyncBehavior. This is the one which you could call RequestNavigate.
public class TabControlRegionSyncBehavior : RegionBehavior, IHostAwareRegionBehavior
{
///<summary>
/// The behavior key for this region sync behavior.
///</summary>
public const string BehaviorKey = "TabControlRegionSyncBehavior";
private static readonly DependencyProperty IsGeneratedProperty =
DependencyProperty.RegisterAttached("IsGenerated", typeof(bool), typeof(TabControlRegionSyncBehavior), null);
private TabControl hostControl;
/// <summary>
/// Gets or sets the <see cref="DependencyObject"/> that the <see cref="IRegion"/> is attached to.
/// </summary>
/// <value>A <see cref="DependencyObject"/> that the <see cref="IRegion"/> is attached to.
/// This is usually a <see cref="FrameworkElement"/> that is part of the tree.</value>
public DependencyObject HostControl
{
get
{
return this.hostControl;
}
set
{
TabControl newValue = value as TabControl;
if (newValue == null)
{
throw new InvalidOperationException(Resources.HostControlMustBeATabControl);
}
if (IsAttached)
{
throw new InvalidOperationException(Resources.HostControlCannotBeSetAfterAttach);
}
this.hostControl = newValue;
}
}
/// <summary>
/// Override this method to perform the logic after the behavior has been attached.
/// </summary>
protected override void OnAttach()
{
if (this.hostControl == null)
{
throw new InvalidOperationException(Resources.HostControlCannotBeNull);
}
this.SynchronizeItems();
this.hostControl.SelectionChanged += this.OnSelectionChanged;
this.Region.ActiveViews.CollectionChanged += this.OnActiveViewsChanged;
this.Region.Views.CollectionChanged += this.OnViewsChanged;
}
/// <summary>
/// Gets the item contained in the <see cref="TabItem"/>.
/// </summary>
/// <param name="tabItem">The container item.</param>
/// <returns>The item contained in the <paramref name="tabItem"/> if it was generated automatically by the behavior; otherwise <paramref name="tabItem"/>.</returns>
protected virtual object GetContainedItem(TabItem tabItem)
{
if (tabItem == null) throw new ArgumentNullException("tabItem");
if ((bool)tabItem.GetValue(IsGeneratedProperty))
{
return tabItem.Content;
}
return tabItem;
}
/// <summary>
/// Override to change how TabItem's are prepared for items.
/// </summary>
/// <param name="item">The item to wrap in a TabItem</param>
/// <param name="parent">The parent <see cref="DependencyObject"/></param>
/// <returns>A tab item that wraps the supplied <paramref name="item"/></returns>
protected virtual TabItem PrepareContainerForItem(object item, DependencyObject parent)
{
TabItem container = item as TabItem;
if (container == null)
{
object dataContext = GetDataContext(item);
container = new TabItem();
container.Content = item;
container.Style = TabControlRegionAdapter.GetItemContainerStyle(parent);
container.DataContext = dataContext; // To run with SL 2
container.Header = dataContext; // To run with SL 3
container.SetValue(IsGeneratedProperty, true);
}
return container;
}
/// <summary>
/// Undoes the effects of the <see cref="PrepareContainerForItem"/> method.
/// </summary>
/// <param name="tabItem">The container element for the item.</param>
protected virtual void ClearContainerForItem(TabItem tabItem)
{
if (tabItem == null) throw new ArgumentNullException("tabItem");
if ((bool)tabItem.GetValue(IsGeneratedProperty))
{
tabItem.Content = null;
}
}
/// <summary>
/// Creates or identifies the element that is used to display the given item.
/// </summary>
/// <param name="item">The item to get the container for.</param>
/// <param name="itemCollection">The parent's <see cref="ItemCollection"/>.</param>
/// <returns>The element that is used to display the given item.</returns>
protected virtual TabItem GetContainerForItem(object item, ItemCollection itemCollection)
{
if (itemCollection == null) throw new ArgumentNullException("itemCollection");
TabItem container = item as TabItem;
if (container != null && ((bool)container.GetValue(IsGeneratedProperty)) == false)
{
return container;
}
foreach (TabItem tabItem in itemCollection)
{
if ((bool)tabItem.GetValue(IsGeneratedProperty))
{
if (tabItem.Content == item)
{
return tabItem;
}
}
}
return null;
}
/// <summary>
/// Return the appropriate data context. If the item is a FrameworkElement it cannot be a data context in Silverlight, so we use its data context.
/// Otherwise, we just us the item as the data context.
/// </summary>
private static object GetDataContext(object item)
{
FrameworkElement frameworkElement = item as FrameworkElement;
return frameworkElement == null ? item : frameworkElement.DataContext;
}
private void SynchronizeItems()
{
List<object> existingItems = new List<object>();
if (this.hostControl.Items.Count > 0)
{
// Control must be empty before "Binding" to a region
foreach (object childItem in this.hostControl.Items)
{
existingItems.Add(childItem);
}
}
foreach (object view in this.Region.Views)
{
TabItem tabItem = this.PrepareContainerForItem(view, this.hostControl);
this.hostControl.Items.Add(tabItem);
}
foreach (object existingItem in existingItems)
{
this.Region.Add(existingItem);
}
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
// e.OriginalSource == null, that's why we use sender.
if (this.hostControl == sender)
{
foreach (TabItem tabItem in e.RemovedItems)
{
object item = this.GetContainedItem(tabItem);
// check if the view is in both Views and ActiveViews collections (there may be out of sync)
if (this.Region.Views.Contains(item) && this.Region.ActiveViews.Contains(item))
{
this.Region.Deactivate(item);
}
}
foreach (TabItem tabItem in e.AddedItems)
{
object item = this.GetContainedItem(tabItem);
if (!this.Region.ActiveViews.Contains(item))
{
this.Region.Activate(item);
}
}
}
}
private void OnActiveViewsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
this.hostControl.SelectedItem = this.GetContainerForItem(e.NewItems[0], this.hostControl.Items);
}
else if (e.Action == NotifyCollectionChangedAction.Remove
&& this.hostControl.SelectedItem != null
&& e.OldItems.Contains(this.GetContainedItem((TabItem)this.hostControl.SelectedItem)))
{
this.hostControl.SelectedItem = null;
}
}
private void OnViewsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
int startingIndex = e.NewStartingIndex;
foreach (object newItem in e.NewItems)
{
TabItem tabItem = this.PrepareContainerForItem(newItem, this.hostControl);
this.hostControl.Items.Insert(startingIndex, tabItem);
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (object oldItem in e.OldItems)
{
TabItem tabItem = this.GetContainerForItem(oldItem, this.hostControl.Items);
this.hostControl.Items.Remove(tabItem);
this.ClearContainerForItem(tabItem);
}
}
}
}
Of course, you'll have to figure out where to call RequestNavigate, such that you can actually cancel the TabSelectionChanging. Unfortunately, this event doesn't exist in WPF. I would resort to the trick recommended by Josh Smith How to Prevent a TabItem from changing
What I understood from your question is that you expect that switching tabs calls IConfirmNavigationRequest. Method from this interface is called when you navigating from view/viewModel implementing this interface.
But, what you experiencing when you switch tabs in TabControl is not Navigation request. All views in TabControl already handled Navigation operation and all views are already in TabControl(Your Region). So what you do when you switch tabs? You only Activating view within your region. Previously active view gets deactivated.
I really don't know what are you trying to accomplish. I cannot imagine why whould I prevent somebody from switching tabs. But you could try that by using IActiveAware interface. You can get the idea from this blog
EDIT
Implement OnDeactivate to ask user whether or not he wants to save changes before deactivating view
Implement OnActivate to call RequestNavigate to already existing View. U can read about Navigating to Existing Views in Prism documentation.
Disable all other tabItems and enable them again after saving changes(bad approach)
I am really not an expert, but I don't think you have more options left
I have a data-bound TreeView and I want to bind SelectedItem. This attached behavior works perfectly without HierarchicalDataTemplate but with it the attached behavior only works one way (UI to data) not the other because now e.NewValue is MyViewModel not TreeViewItem.
This is a code snippet from the attached behavior:
private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var item = e.NewValue as TreeViewItem;
if (item != null)
{
item.SetValue(TreeViewItem.IsSelectedProperty, true);
}
}
This is my TreeView definition:
<Window xmlns:interactivity="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">
<TreeView ItemsSource="{Binding MyItems}" VirtualizingStackPanel.IsVirtualizing="True">
<interactivity:Interaction.Behaviors>
<behaviors:TreeViewSelectedItemBindingBehavior SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
</interactivity:Interaction.Behaviors>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:MyViewModel}" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}"/>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</Window>
If I can get a reference to the TreeView in the attached behavior method OnSelectedItemChanged, maybe I can use the answers in this question to get the TreeViewItem but I don't know how to get there. Does anyone know how and is it the right way to go?
Here is an improved version of the above mentioned attached behavior. It fully supports twoway binding and also works with HeriarchicalDataTemplate and TreeViews where its items are virtualized. Please note though that to find the 'TreeViewItem' that needs to be selected, it will realize (i.e. create) the virtualized TreeViewItems until it finds the right one. This could potentially be a performance problem with big virtualized trees.
/// <summary>
/// Behavior that makes the <see cref="System.Windows.Controls.TreeView.SelectedItem" /> bindable.
/// </summary>
public class BindableSelectedItemBehavior : Behavior<TreeView>
{
/// <summary>
/// Identifies the <see cref="SelectedItem" /> dependency property.
/// </summary>
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(
"SelectedItem",
typeof(object),
typeof(BindableSelectedItemBehavior),
new UIPropertyMetadata(null, OnSelectedItemChanged));
/// <summary>
/// Gets or sets the selected item of the <see cref="TreeView" /> that this behavior is attached
/// to.
/// </summary>
public object SelectedItem
{
get
{
return this.GetValue(SelectedItemProperty);
}
set
{
this.SetValue(SelectedItemProperty, value);
}
}
/// <summary>
/// Called after the behavior is attached to an AssociatedObject.
/// </summary>
/// <remarks>
/// Override this to hook up functionality to the AssociatedObject.
/// </remarks>
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.SelectedItemChanged += this.OnTreeViewSelectedItemChanged;
}
/// <summary>
/// Called when the behavior is being detached from its AssociatedObject, but before it has
/// actually occurred.
/// </summary>
/// <remarks>
/// Override this to unhook functionality from the AssociatedObject.
/// </remarks>
protected override void OnDetaching()
{
base.OnDetaching();
if (this.AssociatedObject != null)
{
this.AssociatedObject.SelectedItemChanged -= this.OnTreeViewSelectedItemChanged;
}
}
private static Action<int> GetBringIndexIntoView(Panel itemsHostPanel)
{
var virtualizingPanel = itemsHostPanel as VirtualizingStackPanel;
if (virtualizingPanel == null)
{
return null;
}
var method = virtualizingPanel.GetType().GetMethod(
"BringIndexIntoView",
BindingFlags.Instance | BindingFlags.NonPublic,
Type.DefaultBinder,
new[] { typeof(int) },
null);
if (method == null)
{
return null;
}
return i => method.Invoke(virtualizingPanel, new object[] { i });
}
/// <summary>
/// Recursively search for an item in this subtree.
/// </summary>
/// <param name="container">
/// The parent ItemsControl. This can be a TreeView or a TreeViewItem.
/// </param>
/// <param name="item">
/// The item to search for.
/// </param>
/// <returns>
/// The TreeViewItem that contains the specified item.
/// </returns>
private static TreeViewItem GetTreeViewItem(ItemsControl container, object item)
{
if (container != null)
{
if (container.DataContext == item)
{
return container as TreeViewItem;
}
// Expand the current container
if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
{
container.SetValue(TreeViewItem.IsExpandedProperty, true);
}
// Try to generate the ItemsPresenter and the ItemsPanel.
// by calling ApplyTemplate. Note that in the
// virtualizing case even if the item is marked
// expanded we still need to do this step in order to
// regenerate the visuals because they may have been virtualized away.
container.ApplyTemplate();
var itemsPresenter =
(ItemsPresenter)container.Template.FindName("ItemsHost", container);
if (itemsPresenter != null)
{
itemsPresenter.ApplyTemplate();
}
else
{
// The Tree template has not named the ItemsPresenter,
// so walk the descendents and find the child.
itemsPresenter = container.GetVisualDescendant<ItemsPresenter>();
if (itemsPresenter == null)
{
container.UpdateLayout();
itemsPresenter = container.GetVisualDescendant<ItemsPresenter>();
}
}
var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);
// Ensure that the generator for this panel has been created.
#pragma warning disable 168
var children = itemsHostPanel.Children;
#pragma warning restore 168
var bringIndexIntoView = GetBringIndexIntoView(itemsHostPanel);
for (int i = 0, count = container.Items.Count; i < count; i++)
{
TreeViewItem subContainer;
if (bringIndexIntoView != null)
{
// Bring the item into view so
// that the container will be generated.
bringIndexIntoView(i);
subContainer =
(TreeViewItem)container.ItemContainerGenerator.
ContainerFromIndex(i);
}
else
{
subContainer =
(TreeViewItem)container.ItemContainerGenerator.
ContainerFromIndex(i);
// Bring the item into view to maintain the
// same behavior as with a virtualizing panel.
subContainer.BringIntoView();
}
if (subContainer == null)
{
continue;
}
// Search the next level for the object.
var resultContainer = GetTreeViewItem(subContainer, item);
if (resultContainer != null)
{
return resultContainer;
}
// The object is not under this TreeViewItem
// so collapse it.
subContainer.IsExpanded = false;
}
}
return null;
}
private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var item = e.NewValue as TreeViewItem;
if (item != null)
{
item.SetValue(TreeViewItem.IsSelectedProperty, true);
return;
}
var behavior = (BindableSelectedItemBehavior)sender;
var treeView = behavior.AssociatedObject;
if (treeView == null)
{
// at designtime the AssociatedObject sometimes seems to be null
return;
}
item = GetTreeViewItem(treeView, e.NewValue);
if (item != null)
{
item.IsSelected = true;
}
}
private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
this.SelectedItem = e.NewValue;
}
}
And for the sake of completeness hier is the implementation of GetVisualDescentants:
/// <summary>
/// Extension methods for the <see cref="DependencyObject" /> type.
/// </summary>
public static class DependencyObjectExtensions
{
/// <summary>
/// Gets the first child of the specified visual that is of tyoe <typeparamref name="T" />
/// in the visual tree recursively.
/// </summary>
/// <param name="visual">The visual to get the visual children for.</param>
/// <returns>
/// The first child of the specified visual that is of tyoe <typeparamref name="T" /> of the
/// specified visual in the visual tree recursively or <c>null</c> if none was found.
/// </returns>
public static T GetVisualDescendant<T>(this DependencyObject visual) where T : DependencyObject
{
return (T)visual.GetVisualDescendants().FirstOrDefault(d => d is T);
}
/// <summary>
/// Gets all children of the specified visual in the visual tree recursively.
/// </summary>
/// <param name="visual">The visual to get the visual children for.</param>
/// <returns>All children of the specified visual in the visual tree recursively.</returns>
public static IEnumerable<DependencyObject> GetVisualDescendants(this DependencyObject visual)
{
if (visual == null)
{
yield break;
}
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
{
var child = VisualTreeHelper.GetChild(visual, i);
yield return child;
foreach (var subChild in GetVisualDescendants(child))
{
yield return subChild;
}
}
}
}
I know this is old question, but perhaps it will be helpful for others. I combined a code from Link
And it looks now:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
using System.Windows.Media;
namespace Behaviors
{
public class BindableSelectedItemBehavior : Behavior<TreeView>
{
#region SelectedItem Property
public object SelectedItem
{
get { return (object)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableSelectedItemBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged));
private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
// if binded to vm collection than this way is not working
//var item = e.NewValue as TreeViewItem;
//if (item != null)
//{
// item.SetValue(TreeViewItem.IsSelectedProperty, true);
//}
var tvi = e.NewValue as TreeViewItem;
if (tvi == null)
{
var tree = ((BindableSelectedItemBehavior)sender).AssociatedObject;
tvi = GetTreeViewItem(tree, e.NewValue);
}
if (tvi != null)
{
tvi.IsSelected = true;
tvi.Focus();
}
}
#endregion
#region Private
private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
SelectedItem = e.NewValue;
}
private static TreeViewItem GetTreeViewItem(ItemsControl container, object item)
{
if (container != null)
{
if (container.DataContext == item)
{
return container as TreeViewItem;
}
// Expand the current container
if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
{
container.SetValue(TreeViewItem.IsExpandedProperty, true);
}
// Try to generate the ItemsPresenter and the ItemsPanel.
// by calling ApplyTemplate. Note that in the
// virtualizing case even if the item is marked
// expanded we still need to do this step in order to
// regenerate the visuals because they may have been virtualized away.
container.ApplyTemplate();
var itemsPresenter =
(ItemsPresenter)container.Template.FindName("ItemsHost", container);
if (itemsPresenter != null)
{
itemsPresenter.ApplyTemplate();
}
else
{
// The Tree template has not named the ItemsPresenter,
// so walk the descendents and find the child.
itemsPresenter = FindVisualChild<ItemsPresenter>(container);
if (itemsPresenter == null)
{
container.UpdateLayout();
itemsPresenter = FindVisualChild<ItemsPresenter>(container);
}
}
var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);
// Ensure that the generator for this panel has been created.
#pragma warning disable 168
var children = itemsHostPanel.Children;
#pragma warning restore 168
for (int i = 0, count = container.Items.Count; i < count; i++)
{
var subContainer = (TreeViewItem)container.ItemContainerGenerator.
ContainerFromIndex(i);
if (subContainer == null)
{
continue;
}
subContainer.BringIntoView();
// Search the next level for the object.
var resultContainer = GetTreeViewItem(subContainer, item);
if (resultContainer != null)
{
return resultContainer;
}
else
{
// The object is not under this TreeViewItem
// so collapse it.
//subContainer.IsExpanded = false;
}
}
}
return null;
}
/// <summary>
/// Search for an element of a certain type in the visual tree.
/// </summary>
/// <typeparam name="T">The type of element to find.</typeparam>
/// <param name="visual">The parent element.</param>
/// <returns></returns>
private static T FindVisualChild<T>(Visual visual) where T : Visual
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
{
Visual child = (Visual)VisualTreeHelper.GetChild(visual, i);
if (child != null)
{
T correctlyTyped = child as T;
if (correctlyTyped != null)
{
return correctlyTyped;
}
T descendent = FindVisualChild<T>(child);
if (descendent != null)
{
return descendent;
}
}
}
return null;
}
#endregion
#region Protected
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
if (AssociatedObject != null)
{
AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
}
}
#endregion
}
}
If you find, like I did, that this answer sometimes crashes because itemPresenter is null, then this modification to that solution might work for you.
Change OnSelectedItemChanged to this (if the Tree isn't loaded yet, then it waits until the Tree is loaded and tries again):
private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
Action<TreeViewItem> selectTreeViewItem = tvi2 =>
{
if (tvi2 != null)
{
tvi2.IsSelected = true;
tvi2.Focus();
}
};
var tvi = e.NewValue as TreeViewItem;
if (tvi == null)
{
var tree = ((BindableTreeViewSelectedItemBehavior) sender).AssociatedObject;
if (!tree.IsLoaded)
{
RoutedEventHandler handler = null;
handler = (sender2, e2) =>
{
tvi = GetTreeViewItem(tree, e.NewValue);
selectTreeViewItem(tvi);
tree.Loaded -= handler;
};
tree.Loaded += handler;
return;
}
tvi = GetTreeViewItem(tree, e.NewValue);
}
selectTreeViewItem(tvi);
}
I am using simple validations using the INotifyDataErrorInfo implementation in silverlight.
When submitting I am validating all properties to show all the errors.
I need to get the focus back to the first control with a validation error, when validation occurs.
Do we have a way to do this? Any suggestions?
Better late than never:)
I've implemented this behavior.
First you need to subscribe to your ViewModel ErrorsChanged and PropertyChanged methods. I am doing this in my constructor:
/// <summary>
/// Initializes new instance of the View class.
/// </summary>
public View(ViewModel viewModel)
{
if (viewModel == null)
throw new ArgumentNullException("viewModel");
// Initialize the control
InitializeComponent(); // exception
// Set view model to data context.
DataContext = viewModel;
viewModel.PropertyChanged += new PropertyChangedEventHandler(_ViewModelPropertyChanged);
viewModel.ErrorsChanged += new EventHandler<DataErrorsChangedEventArgs>(_ViewModelErrorsChanged);
}
Then write handlers for this events:
/// <summary>
/// If model errors has changed and model still have errors set flag to true,
/// if we dont have errors - set flag to false.
/// </summary>
/// <param name="sender">Ignored.</param>
/// <param name="e">Ignored.</param>
private void _ViewModelErrorsChanged(object sender, DataErrorsChangedEventArgs e)
{
if ((this.DataContext as INotifyDataErrorInfo).HasErrors)
_hasErrorsRecentlyChanged = true;
else
_hasErrorsRecentlyChanged = false;
}
/// <summary>
/// Iterate over view model visual childrens.
/// </summary>
/// <param name="sender">Ignored.</param>
/// <param name="e">Ignored.</param>
private void _ViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if ((this.DataContext as INotifyDataErrorInfo).HasErrors)
_LoopThroughControls(this);
}
And finally add method:
/// <summary>
/// If we have error and we haven't already set focus - set focus to first control with error.
/// </summary>
/// <remarks>Recursive.</remarks>
/// <param name="parent">Parent element.</param>
private void _LoopThroughControls(UIElement parent)
{
// Check that we have error and we haven't already set focus
if (!_hasErrorsRecentlyChanged)
return;
int count = VisualTreeHelper.GetChildrenCount(parent);
// VisualTreeHelper.GetChildrenCount for TabControl will always return 0, so we need to
// do this branch of code.
if (parent.GetType().Equals(typeof(TabControl)))
{
TabControl tabContainer = ((TabControl)parent);
foreach (TabItem tabItem in tabContainer.Items)
{
if (tabItem.Content == null)
continue;
_LoopThroughControls(tabItem.Content as UIElement);
}
}
// If element has childs.
if (count > 0)
{
for (int i = 0; i < count; i++)
{
UIElement child = (UIElement)VisualTreeHelper.GetChild(parent, i);
if (child is System.Windows.Controls.Control)
{
var control = (System.Windows.Controls.Control)child;
// If control have error - we found first control, set focus to it and
// set flag to false.
if ((bool)control.GetValue(Validation.HasErrorProperty))
{
_hasErrorsRecentlyChanged = false;
control.Focus();
return;
}
}
_LoopThroughControls(child);
}
}
}