From post WPF: How to programmatically remove focus from a TextBox, I know how to set a TextBox's focus back to its parent using the following code:
// Move to a parent that can take focus
FrameworkElement parent = (FrameworkElement)textBox.Parent;
while (parent != null && parent is IInputElement
&& !((IInputElement)parent).Focusable)
{
parent = (FrameworkElement)parent.Parent;
}
DependencyObject scope = FocusManager.GetFocusScope(textBox);
FocusManager.SetFocusedElement(scope, parent as IInputElement);
Is there any way to generalize this code (like template functions) to make it also work for other items like ComboBox, Canvas, Image etc.
It should be relatively straightforward:
FrameworkElement ctrl = control; //or whatever you're passing in, since all controls are FrameworkElements.
// Move to a parent that can take focus
FrameworkElement parent = (FrameworkElement)ctrl.Parent;
while (parent != null && parent is IInputElement
&& !((IInputElement)parent).Focusable)
{
parent = (FrameworkElement)parent.Parent;
}
DependencyObject scope = FocusManager.GetFocusScope(ctrl); //can pass in ctrl here because FrameworkElement inherits from DependencyObject
FocusManager.SetFocusedElement(scope, parent as IInputElement);
This works because all controls inherit from FrameworkElement which inherits from DependencyObject. So you can set ctrl to any type of control you want: ComboBox, TextBox, Button, Canvas, etc.
Yes, there is such a possibility, in such cases, implement attached behavior. Logic at work with a focus must fit into DependencyPropertyChangedEvent handler.
Here is an example for your case:
XAML
<Window x:Class="RemoveFocusHelp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:this="clr-namespace:RemoveFocusHelp"
WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TextBox Name="TestTextBox"
this:RemoveFocusBehavior.IsRemoveFocus="False"
Width="100"
Height="25"
Text="TestText" />
<Button Name="CreateFocus"
Width="100"
Height="30"
Content="Create Focus"
HorizontalAlignment="Left"
Click="CreateFocus_Click" />
<Button Name="RemoveFocus"
Focusable="False"
Width="100"
Height="30"
Content="Remove Focus"
HorizontalAlignment="Right"
Click="RemoveFocus_Click" />
</Grid>
</Window>
Code-behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void CreateFocus_Click(object sender, RoutedEventArgs e)
{
TestTextBox.Focus();
}
private void RemoveFocus_Click(object sender, RoutedEventArgs e)
{
RemoveFocusBehavior.SetIsRemoveFocus(TestTextBox, true);
}
}
public class RemoveFocusBehavior
{
#region IsRemoveFocus Dependency Property
public static readonly DependencyProperty IsRemoveFocusProperty;
public static void SetIsRemoveFocus(DependencyObject DepObject, bool value)
{
DepObject.SetValue(IsRemoveFocusProperty, value);
}
public static bool GetIsRemoveFocus(DependencyObject DepObject)
{
return (bool)DepObject.GetValue(IsRemoveFocusProperty);
}
static RemoveFocusBehavior()
{
IsRemoveFocusProperty = DependencyProperty.RegisterAttached("IsRemoveFocus",
typeof(bool),
typeof(RemoveFocusBehavior),
new UIPropertyMetadata(false, IsRemoveFocusTurn));
}
#endregion
#region IsRemoveFocus Property Metadata
private static void IsRemoveFocusTurn(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
Control control = sender as Control;
if (control == null)
{
return;
}
if (e.NewValue is bool && ((bool)e.NewValue) == true)
{
FrameworkElement parent = (FrameworkElement)control.Parent;
while (parent != null && parent is IInputElement
&& !((IInputElement)parent).Focusable)
{
parent = (FrameworkElement)parent.Parent;
}
DependencyObject scope = FocusManager.GetFocusScope(control);
FocusManager.SetFocusedElement(scope, parent as IInputElement);
}
}
#endregion
}
Project is available at this link.
Related
I have a custom control named CustomTreeView which contains a TreeView. I would like to expose the SelectedItem Property of the TreeView to users of the custom control.
For that I tried to add a new dependency property to the custom control and bind that property to the SelectedItem Property of the TreeView.
Unfortunatly I seem to be getting it wrong. Could you take a look?
TreeView.xaml
<UserControl x:Class="Fis.UI.Windows.BacNet.Views.RestructuredView.View.Controls.CustomTreeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Selected="{Binding ElementName=treeView, Path=SelectedItem}">
<TreeView x:Name="treeView"/>
</UserControl>
TreeView.xaml.cs
public partial class CustomTreeView : UserControl
{
public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.Register(
"Selected", typeof(Node),
typeof(TreeView)
);
public Node Selected
{
get { return (Node)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
public TreeView()
{
InitializeComponent();
}
}
Thanks!
Part of your solution is in this answer here. It's a link-only answer with a dead link (or was -- I just improved it), but it does mention the key point.
You can't bind TreeView.SelectedItem. Your dependency property definition is broken in multiple ways and it should be named SelectedItem in accordance with standard WPF practice. Here's the usercontrol code behind, which defines the dependency property along with event handlers as a substitute for the binding.
public partial class CustomTreeView : UserControl
{
public CustomTreeView()
{
InitializeComponent();
}
#region SelectedItem Property
public Node SelectedItem
{
get { return (Node)GetValue(SelectedProperty); }
set { SetValue(SelectedProperty, value); }
}
public static readonly DependencyProperty SelectedProperty =
DependencyProperty.Register(nameof(SelectedItem), typeof(Node), typeof(CustomTreeView),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
Selected_PropertyChanged)
{ DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });
protected static void Selected_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as CustomTreeView).OnSelectedChanged(e.OldValue);
}
private void OnSelectedChanged(object oldValue)
{
if (SelectedItem != treeView.SelectedItem)
{
var tvi = treeView.ItemContainerGenerator.ContainerFromItem(SelectedItem) as TreeViewItem;
if (tvi != null)
{
tvi.IsSelected = true;
}
}
}
#endregion SelectedItem Property
private void treeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (SelectedItem != e.NewValue)
{
SelectedItem = e.NewValue as Node;
}
}
}
And here's the TreeView in the UserControl XAML. I'm omitting ItemsSource and ItemTemplate as irrelevant.
<TreeView
x:Name="treeView"
SelectedItemChanged="treeView_SelectedItemChanged"
/>
And here's the snippet from MainWindow.xaml I used for testing it:
<StackPanel>
<local:CustomTreeView x:Name="treeControl" />
<Label
Content="{Binding SelectedItem.Text, ElementName=treeControl}"
/>
</StackPanel>
My Node class has a Text property and I'm populating the tree via the DataContext to save having to set up another dependency property for the items.
Well to display "dynamic" data an easy way is to use an ItemsControl (with, say, a WrapPanel as item template).
Now I wish for my application, a rich text box filled with runs is ideal. - The (number & data) of the runs depends on an observable collection in my viewmodel. If I would use a WrapPanel instead of a RichTextBox the itemscontrol code would look like:
<ItemsControl ItemsSource="{Binding Data}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True">
</WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Now I tried using a richtextbox in my usercontrol, the xaml for the usercontrol then looks like:
<UserControl x:Class="testit.MyControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:testit"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:DispData}">
<TextBlock>
<Run Text="{Binding Text}"></Run>
</TextBlock>
</DataTemplate>
</UserControl.Resources>
<StackPanel>
<RichTextBox IsReadOnly="True" IsDocumentEnabled="True" VerticalScrollBarVisibility="Auto">
<FlowDocument>
<ItemsControl ItemsSource="{Binding Data}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Paragraph IsItemsHost="True">
</Paragraph>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</FlowDocument>
</RichTextBox>
<ItemsControl ItemsSource="{Binding Data}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True">
</WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>
</UserControl>
The viewmodel which is bound to the datacontext of the usercontrol is:
namespace testit
{
class ViewModel : INotifyPropertyChanged
{
private readonly ObservableCollection<DispData> _data =
new ObservableCollection<DispData>();
public ReadOnlyObservableCollection<DispData> Data { get; private set; }
public ViewModel() {
Data = new ReadOnlyObservableCollection<DispData>(_data);
_data.Add(new DispData("hello"));
_data.Add(new DispData("world"));
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
This gives a lot of errors with the RichTextBox (first error is that ItemsControl can't be placed there, but another is that Paragraph doesn't have the IsItemsHost property. -- I also wish to stress that if I comment out the richtextbox xaml, the xaml for the wrap panel does work: so it's not the bindings or anything that is wrong.
Can a RichTextBox even be used with an ItemsControl - and if not, how would I fill the content of the textbox in a MVVM fashion?
You should check out this article on how to write your own items control that is compatible with FlowDocument or RichTextBox. Code sample can be found at this location
Once you download the controls, update if-else condition in GenerateContent() method in ItemsContent as following to add support for Paragraph and Inlines.
private void GenerateContent(DataTemplate itemsPanel, DataTemplate itemTemplate, IEnumerable itemsSource)
{
....
if (panel is Section)
((Section)panel).Blocks.Add(Helpers.ConvertToBlock(data, element));
else if (panel is TableRowGroup)
((TableRowGroup)panel).Rows.Add((TableRow)element);
else if (panel is Paragraph && element is Inline)
((Paragraph)panel).Inlines.Add((Inline)element);
else
throw new Exception(String.Format("Don't know how to add an instance of {0} to an instance of {1}", element.GetType(), panel.GetType()));
And update your XAML to:
<RichTextBox IsReadOnly="True" IsDocumentEnabled="True" VerticalScrollBarVisibility="Auto">
<FlowDocument>
<flowdoc:ItemsContent ItemsSource ItemsSource="{Binding Data}">
<flowdoc:ItemsContent.ItemsPanel>
<DataTemplate>
<flowdoc:Fragment>
<Paragraph flowdoc:Attached.IsItemsHost="True" />
</flowdoc:Fragment>
</DataTemplate>
</flowdoc:ItemsContent.ItemsPanel>
<flowdoc:ItemsContent.ItemTemplate>
<DataTemplate>
<flowdoc:Fragment>
<flowdoc:BindableRun BoundText="{Binding Text}" />
</flowdoc:Fragment>
</DataTemplate>
</flowdoc:ItemsContent.ItemTemplate>
</flowdoc:ItemsContent>
</FlowDocument>
</RichTextBox>
EDIT - 1
As #ed-plunkett suggested, sharing relevant code here (in case the external link doesn't work)
In order to be able to use Run in item-template (similar to label or textblock); you will need to extend Run to add a bindable property.
public class BindableRun : Run
{
public static readonly DependencyProperty BoundTextProperty = DependencyProperty.Register("BoundText", typeof(string), typeof(BindableRun), new PropertyMetadata(OnBoundTextChanged));
public BindableRun()
{
Helpers.FixupDataContext(this);
}
private static void OnBoundTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((Run)d).Text = (string)e.NewValue;
}
public String BoundText
{
get { return (string)GetValue(BoundTextProperty); }
set { SetValue(BoundTextProperty, value); }
}
}
Next you would need is the ability to mark a container control as items host; that can be done by defining an attached property.
public class Attached
{
private static readonly DependencyProperty IsItemsHostProperty = DependencyProperty.RegisterAttached("IsItemsHost", typeof(bool), typeof(Attached), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.NotDataBindable, OnIsItemsHostChanged));
private static readonly DependencyProperty ItemsHostProperty = DependencyProperty.RegisterAttached("ItemsHost", typeof(FrameworkContentElement), typeof(Attached), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.NotDataBindable));
public static bool GetIsItemsHost(DependencyObject target)
{
return (bool)target.GetValue(IsItemsHostProperty);
}
public static void SetIsItemsHost(DependencyObject target, bool value)
{
target.SetValue(IsItemsHostProperty, value);
}
private static void SetItemsHost(FrameworkContentElement element)
{
FrameworkContentElement parent = element;
while (parent.Parent != null)
parent = (FrameworkContentElement)parent.Parent;
parent.SetValue(ItemsHostProperty, element);
}
public static FrameworkContentElement GetItemsHost(DependencyObject dp)
{
return (FrameworkContentElement)dp.GetValue(ItemsHostProperty);
}
private static void OnIsItemsHostChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
FrameworkContentElement element = (FrameworkContentElement)d;
if (element.IsInitialized)
SetItemsHost(element);
else
element.Initialized += ItemsHost_Initialized;
}
}
private static void ItemsHost_Initialized(object sender, EventArgs e)
{
FrameworkContentElement element = (FrameworkContentElement)sender;
element.Initialized -= ItemsHost_Initialized;
SetItemsHost(element);
}
}
A fragment control that you can use to embed FrameworkContentElement inside DataTemplate.
[ContentProperty("Content")]
public class Fragment : FrameworkElement
{
private static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(FrameworkContentElement), typeof(Fragment));
public FrameworkContentElement Content
{
get
{
return (FrameworkContentElement)GetValue(ContentProperty);
}
set
{
SetValue(ContentProperty, value);
}
}
}
And, finally the items control itself:, that does the major heavy lifting:
public class ItemsContent : Section
{
private static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(ItemsContent), new PropertyMetadata(OnItemsSourceChanged));
private static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(ItemsContent), new PropertyMetadata(OnItemTemplateChanged));
private static readonly DependencyProperty ItemsPanelProperty = DependencyProperty.Register("ItemsPanel", typeof(DataTemplate), typeof(ItemsContent), new PropertyMetadata(OnItemsPanelChanged));
public ItemsContent()
{
Helpers.FixupDataContext(this);
Loaded += ItemsContent_Loaded;
}
private void ItemsContent_Loaded(object sender, RoutedEventArgs e)
{
GenerateContent(ItemsPanel, ItemTemplate, ItemsSource);
}
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public DataTemplate ItemsPanel
{
get { return (DataTemplate)GetValue(ItemsPanelProperty); }
set { SetValue(ItemsPanelProperty, value); }
}
private void GenerateContent(DataTemplate itemsPanel, DataTemplate itemTemplate, IEnumerable itemsSource)
{
Blocks.Clear();
if (itemTemplate != null && itemsSource != null)
{
FrameworkContentElement panel = null;
foreach (object data in itemsSource)
{
if (panel == null)
{
if (itemsPanel == null)
panel = this;
else
{
FrameworkContentElement p = Helpers.LoadDataTemplate(itemsPanel);
if (!(p is Block))
throw new Exception("ItemsPanel must be a block element");
Blocks.Add((Block)p);
panel = Attached.GetItemsHost(p);
if (panel == null)
throw new Exception("ItemsHost not found. Did you forget to specify Attached.IsItemsHost?");
}
}
FrameworkContentElement element = Helpers.LoadDataTemplate(itemTemplate);
element.DataContext = data;
Helpers.UnFixupDataContext(element);
if (panel is Section)
((Section)panel).Blocks.Add(Helpers.ConvertToBlock(data, element));
else if (panel is TableRowGroup)
((TableRowGroup)panel).Rows.Add((TableRow)element);
else if (panel is Paragraph && element is Inline)
((Paragraph)panel).Inlines.Add((Inline)element);
else
throw new Exception(String.Format("Don't know how to add an instance of {0} to an instance of {1}", element.GetType(), panel.GetType()));
}
}
}
private void GenerateContent()
{
GenerateContent(ItemsPanel, ItemTemplate, ItemsSource);
}
private void OnItemsSourceChanged(IEnumerable newValue)
{
if (IsLoaded)
GenerateContent(ItemsPanel, ItemTemplate, newValue);
}
private void OnItemTemplateChanged(DataTemplate newValue)
{
if (IsLoaded)
GenerateContent(ItemsPanel, newValue, ItemsSource);
}
private void OnItemsPanelChanged(DataTemplate newValue)
{
if (IsLoaded)
GenerateContent(newValue, ItemTemplate, ItemsSource);
}
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ItemsContent)d).OnItemsSourceChanged((IEnumerable)e.NewValue);
}
private static void OnItemTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ItemsContent)d).OnItemTemplateChanged((DataTemplate)e.NewValue);
}
private static void OnItemsPanelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ItemsContent)d).OnItemsPanelChanged((DataTemplate)e.NewValue);
}
}
Static helper methods:
internal static class Helpers
{
/// <summary>
/// If you use a bindable flow document element more than once, you may encounter a "Collection was modified" exception.
/// The error occurs when the binding is updated because of a change to an inherited dependency property. The most common scenario
/// is when the inherited DataContext changes. It appears that an inherited properly like DataContext is propagated to its descendants.
/// When the enumeration of descendants gets to a BindableXXX, the dependency properties of that element change according to the new
/// DataContext, which change the (non-dependency) properties. However, for some reason, changing the flow content invalidates the
/// enumeration and raises an exception.
/// To work around this, one can either DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType=FrameworkElement}}"
/// in code. This is clumsy, so every derived type calls this function instead (which performs the same thing).
/// See http://code.logos.com/blog/2008/01/data_binding_in_a_flowdocument.html
/// </summary>
/// <param name="element"></param>
public static void FixupDataContext(FrameworkContentElement element)
{
Binding b = new Binding(FrameworkContentElement.DataContextProperty.Name);
// another approach (if this one has problems) is to bind to an ancestor by ElementName
b.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(FrameworkElement), 1);
element.SetBinding(FrameworkContentElement.DataContextProperty, b);
}
private static bool InternalUnFixupDataContext(DependencyObject dp)
{
// only consider those elements for which we've called FixupDataContext(): they all belong to this namespace
if (dp is FrameworkContentElement && dp.GetType().Namespace == typeof(Helpers).Namespace)
{
Binding binding = BindingOperations.GetBinding(dp, FrameworkContentElement.DataContextProperty);
if (binding != null
&& binding.Path != null && binding.Path.Path == FrameworkContentElement.DataContextProperty.Name
&& binding.RelativeSource != null && binding.RelativeSource.Mode == RelativeSourceMode.FindAncestor && binding.RelativeSource.AncestorType == typeof(FrameworkElement) && binding.RelativeSource.AncestorLevel == 1)
{
BindingOperations.ClearBinding(dp, FrameworkContentElement.DataContextProperty);
return true;
}
}
// as soon as we have disconnected a binding, return. Don't continue the enumeration, since the collection may have changed
foreach (object child in LogicalTreeHelper.GetChildren(dp))
if (child is DependencyObject)
if (InternalUnFixupDataContext((DependencyObject)child))
return true;
return false;
}
public static void UnFixupDataContext(DependencyObject dp)
{
while (InternalUnFixupDataContext(dp))
;
}
/// <summary>
/// Convert "data" to a flow document block object. If data is already a block, the return value is data recast.
/// </summary>
/// <param name="dataContext">only used when bindable content needs to be created</param>
/// <param name="data"></param>
/// <returns></returns>
public static Block ConvertToBlock(object dataContext, object data)
{
if (data is Block)
return (Block)data;
else if (data is Inline)
return new Paragraph((Inline)data);
else if (data is BindingBase)
{
BindableRun run = new BindableRun();
if (dataContext is BindingBase)
run.SetBinding(BindableRun.DataContextProperty, (BindingBase)dataContext);
else
run.DataContext = dataContext;
run.SetBinding(BindableRun.BoundTextProperty, (BindingBase)data);
return new Paragraph(run);
}
else
{
Run run = new Run();
run.Text = (data == null) ? String.Empty : data.ToString();
return new Paragraph(run);
}
}
public static FrameworkContentElement LoadDataTemplate(DataTemplate dataTemplate)
{
object content = dataTemplate.LoadContent();
if (content is Fragment)
return (FrameworkContentElement)((Fragment)content).Content;
else if (content is TextBlock)
{
InlineCollection inlines = ((TextBlock)content).Inlines;
if (inlines.Count == 1)
return inlines.FirstInline;
else
{
Paragraph paragraph = new Paragraph();
// we can't use an enumerator, since adding an inline removes it from its collection
while (inlines.FirstInline != null)
paragraph.Inlines.Add(inlines.FirstInline);
return paragraph;
}
}
else
throw new Exception("Data template needs to contain a <Fragment> or <TextBlock>");
}
}
I have the following UserControl that has an attached behavior on it. At the moment I have to select one of the DataGrid items in order for it to run the behavior.
Only guessing here but it's kind of understandable because these containers are not focusable or are not supposed to register keyboard events, so it goes to the next child available???
I could place this functionality on the window however this UserControl can get swapped out with another UserControl and I prefer for it not be be lying around.
This is just a unique case in my app where the only thing on the window is the DataGrid and I don't necessarily want to select a DataGrid item to begin the behavior.
My question is: Is it possible to get the following behavior to work? Either on the UserControl, Grid or maybe some other control that would permit this.
<UserControl x:Class="UserManagement.LaunchPad"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:UserManagement.Helpers"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<i:Interaction.Behaviors>
<helpers:OpenPopupBehaviors FindPopupObject="{Binding ElementName=PopupFind}"
GotoPopupObject="{Binding ElementName=PopupGoto}" />
</i:Interaction.Behaviors>
<Grid>
<DockPanel>
<DataGrid />
</DockPanel>
<Popup Name="PopupGoto" />
<Popup Name="PopupFilter "/>
</Grid>
</UserControl>
Here is the behavior:
class OpenPopupBehaviors : Behavior<UserControl>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.KeyDown += _KeyBoardBehaviorKeyDown;
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.KeyDown -= _KeyBoardBehaviorKeyDown;
}
void _KeyBoardBehaviorKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.F && (Keyboard.Modifiers & (ModifierKeys.Control)) == (ModifierKeys.Control))
{
var popup = FindPopupObject as UserManagement.Controls.NonTopmostPopup;
if (popup == null)
return;
popup.IsOpen = true;
}
if (e.Key == Key.G && (Keyboard.Modifiers & (ModifierKeys.Control)) == (ModifierKeys.Control))
{
var popup = GotoPopupObject as Popup;
if (popup == null)
return;
popup.IsOpen = true;
}
}
public static readonly DependencyProperty FindPopupObjectProperty =
DependencyProperty.RegisterAttached("FindPopupObject", typeof(object), typeof(OpenPopupBehaviors), new UIPropertyMetadata(null));
public object FindPopupObject
{
get { return (object)GetValue(FindPopupObjectProperty); }
set { SetValue(FindPopupObjectProperty, value); }
}
public static readonly DependencyProperty GotoPopupObjectProperty =
DependencyProperty.RegisterAttached("GotoPopupObject", typeof(object), typeof(OpenPopupBehaviors), new UIPropertyMetadata(null));
public object GotoPopupObject
{
get { return (object)GetValue(GotoPopupObjectProperty); }
set { SetValue(GotoPopupObjectProperty, value); }
}
}
So far I can see is that the focus if your issue
you can make use of Preview events to receive the events even if the focus is on control's child or their descendants
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.PreviewKeyDown += _KeyBoardBehaviorKeyDown;
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.PreviewKeyDown-= _KeyBoardBehaviorKeyDown;
}
I have changed KeyDown to PreviewKeyDown
above solution will work as long as the AssociatedObject have focus on it or any child within it. if you look forward to control it globally then perhaps you may need to attach the events to top level elements eg. Window
so in order to do that you need to find the appropriate parent of AssociatedObject and attach the event to the same.
also make sure that the following cast is valid
FindPopupObject as UserManagement.Controls.NonTopmostPopup;
it could be
FindPopupObject as Popup;
I was faced with the next misunderstanding.
Preamble:
I have wpf application with next essential UI parts: RadioButtons and some control that use dropdown based on Popup (in combobox manner). According to some logic every radiobutton hook PreviewMouseDown event and do some calculations.
In the next scenario,
User opens popup (do not select something, popup just staying open)
User click on radiobutton
PreviewMouseDown will not be fired for radiobutton as expected (because of Popup feature).
And my aim is firing PreviewMouseDown for RadioButton despite of one.
Attempts to solve:
Fast and dirty solution is: hook PreviewMouseDown for Popup and re-fire PreviewMouseDown event with new source if required, using radiobutton as source. New source can be obtained via MouseButtonEventArgs.MouseDevice.DirectlyOver. The next piece of code do that (event is re-fired only if Popup "eat" PreviewMouseDown for outer click):
private static void GrantedPopupPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
var popup = sender as Popup;
if(popup == null)
return;
var realSource = e.MouseDevice.DirectlyOver as FrameworkElement;
if(realSource == null || !realSource.IsLoaded)
return;
var parent = LayoutTreeHelper.GetParent<Popup>(realSource);
if(parent == null || !Equals(parent, popup ))
{
e.Handled = true;
var args = new MouseButtonEventArgs(e.MouseDevice,
e.Timestamp,
e.ChangedButton)
{
RoutedEvent = UIElement.PreviewMouseDownEvent,
Source = e.MouseDevice.DirectlyOver,
};
realSource.RaiseEvent(args);
}
}
This works well when I'm attaching that handler to Popup.PreviewMouseDown directly via Behavior and do not work (PreviewMouseDown isn't fired for radiobutton) if I'm attaching one via EventManager.RegisterClassHandler (aim is to avoid attaching behavior to every Popup that can occure on page with these radiobuttons):
EventManager.RegisterClassHandler(
typeof (Popup),
PreviewMouseDownEvent,
new MouseButtonEventHandler(GrantedPopupPreviewMouseDown));
Debugger showed that e.MouseDevice.DirectlyOver (see code above) is Popup, not Radiobutton (as it is was when I've attached handler via Behavior)!
Question:
How and whyMouseButtonEventArgs can be different for the same action, if eventhandler attached in two different ways?
Can someone explaing this behavior?
Thanks a lot.
The combo box is provided as a way for users to select from a group of options, and you likely want to do that. But it also has other contracts. It says that the user should be focused on this and only this task. But that is not your situation. You want to show the options, have them hide able, and allow the user to do other things while they are shown.
I think instead of combo boxes you want some other control. My suggestion is to use an expander that contains a listbox. Given:
class NotificationObject : INotifyPropertyChanged
{
public void RaisePropertyChanged(string name)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class ComboEntry : NotificationObject
{
public string Name { get; private set; }
private string _option = "Off";
public string Option
{
get { return _option; }
set { _option = value; RaisePropertyChanged("Option"); }
}
public ComboEntry()
{
Name = Guid.NewGuid().ToString();
}
}
class MyDataContext : NotificationObject
{
public ObservableCollection<ComboEntry> Entries { get; private set; }
private ComboEntry _selectedEntry;
public ComboEntry SelectedEntry
{
get { return _selectedEntry; }
set { _selectedEntry = value; RaisePropertyChanged("SelectedEntry"); }
}
public MyDataContext()
{
Entries = new ObservableCollection<ComboEntry>
{
new ComboEntry(),
new ComboEntry(),
new ComboEntry()
};
SelectedEntry = Entries.FirstOrDefault();
}
public void SetOption(string value)
{
Entries
.ToList()
.ForEach(entry => entry.Option = value);
}
}
I think you want the following XAML:
<Window x:Class="RadioInCombo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:RadioInCombo"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:MyDataContext x:Key="myDataContext" />
<DataTemplate x:Key="ComboEntryTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<Border Width="5" />
<TextBlock Text="{Binding Option}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<StackPanel DataContext="{StaticResource myDataContext}">
<RadioButton x:Name="OnButton"
Content="On"
PreviewMouseDown="OnButton_PreviewMouseDown" />
<RadioButton x:Name="OffButton"
Content="Off"
PreviewMouseDown="OffButton_PreviewMouseDown" />
<Expander Header="{Binding SelectedEntry}"
HeaderTemplate="{StaticResource ComboEntryTemplate}">
<ListBox ItemsSource="{Binding Entries}"
ItemTemplate="{StaticResource ComboEntryTemplate}" />
</Expander>
</StackPanel>
</Window>
And the following code-behind:
private MyDataContext GetMyDataContext()
{
var candidate = FindResource("myDataContext") as MyDataContext;
if (candidate == null) throw new ApplicationException("Could not locate the myDataContext object");
return candidate;
}
private void OnButton_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
GetMyDataContext().SetOption("On");
}
private void OffButton_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
GetMyDataContext().SetOption("Off");
}
I have a DataTemplate defined as follows
<DataTemplate x:Key="PasswordViewerTemplate">
<StackPanel>
<TextBlock Text="{Binding PasswordChar, ElementName=this}"
Visibility="Visible" />
<TextBox Text="{Binding PasswordText}"
Visibility="Collapsed" />
</StackPanel>
</DataTemplate>
I want to be able to toggle visibilities of the TextBlock and the TextBox each time the user clicks on the StackPanel. I tried setting a MouseLeftButtonUp event handler on the StackPanel but this throws an exception
Object reference not set to an instance of an object
Is there another way to achieve this? Maybe in XAML itself using triggers?
Also, this might be relevant. The above template is one of two that is applied to a ListBox by a template selector. The ListBox itself is within a Grid and both templates are defined within the Grid.Resources section.
EDIT 1
I tried setting the event as follows
<StackPanel MouseLeftButtonUp="OnPasswordViewerMouseLeftButtonUp">
...
</StackPanel>
private void OnPasswordViewerMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
var sp = sender as StackPanel;
if( ( sp == null ) || ( sp.Children.Count != 2 ) ) {
return;
}
var passwordText = sp.Children[0] as TextBlock;
var plainText = sp.Children[1] as TextBox;
if( ( passwordText == null ) || ( plainText == null ) ) {
return;
}
passwordText.Visibility = ( passwordText.Visibility == Visibility.Visible ) ?
Visibility.Collapsed : Visibility.Visible;
plainText.Visibility = ( plainText.Visibility == Visibility.Visible ) ?
Visibility.Collapsed : Visibility.Visible;
}
One of the solutions is to bind visibility of the TextBox and TextBlock to properties of the class which is used as DataContext for the StackPanel. Here is a sample implementation:
Xaml code:
<Grid>
<Grid.Resources>
<DataTemplate x:Key="PasswordViewerTemplate">
<StackPanel PreviewMouseUp="StackPanel_PreviewMouseUp">
<TextBlock Text="{Binding Path=PasswordChar}"
Visibility="{Binding Path=TextBlockVisibility}" />
<TextBox Text="{Binding Path=PasswordText}"
Visibility="{Binding Path=TextBoxVisibility}" />
</StackPanel>
</DataTemplate>
</Grid.Resources>
<ListBox x:Name="lbox" ItemTemplate="{StaticResource ResourceKey=PasswordViewerTemplate}" ItemsSource="{Binding}"/>
</Grid>
And C# code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ObservableCollection<Some> items = new ObservableCollection<Some>();
for (int i = 0; i < 10; i++)
{
items.Add(new Some(string.Format("passwordChar {0}", i + 1), string.Format("passwordText {0}", i + 1), Visibility.Visible, Visibility.Collapsed));
}
this.lbox.ItemsSource = items;
}
private void StackPanel_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
Some some = (sender as StackPanel).DataContext as Some;
some.TextBlockVisibility = ToggleVisibility(some.TextBlockVisibility);
some.TextBoxVisibility = ToggleVisibility(some.TextBoxVisibility);
}
private Visibility ToggleVisibility(Visibility visibility)
{
return visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
}
}
public class Some:INotifyPropertyChanged
{
private string _passwordChar;
private string _passwordText;
private Visibility _textBlockVisibility, _textBoxVisibility;
public string PasswordChar { get { return this._passwordChar; } set { this._passwordChar = value; } }
public string PasswordText { get { return this._passwordText; } set { this._passwordText = value; } }
public Visibility TextBlockVisibility
{
get { return this._textBlockVisibility; }
set
{
this._textBlockVisibility = value;
RaisePropertyChanged("TextBlockVisibility");
}
}
public Visibility TextBoxVisibility
{
get { return this._textBoxVisibility; }
set
{
this._textBoxVisibility = value;
RaisePropertyChanged("TextBoxVisibility");
}
}
public Some(string passwordChar, string passwordText, Visibility textBlockVisibility, Visibility textBoxVisibility)
{
this._passwordChar = passwordChar;
this._passwordText = passwordText;
this._textBlockVisibility = textBlockVisibility;
this._textBoxVisibility = textBoxVisibility;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
Why dont you bind the item's visibility in your view model?
an Example.
<Textblock Test="{Binding passwordText,ElementName=This}" Visibility="{Binding passwordTextVisibility}"/>
in your ViewModel say
public Visibility passwordTextVisibility
{
getters and setters here
}
and on your mouse event, you would need some sort of routed event inside the stack panel. an example:
inside the stack panel you would need mouse. whatever you need. read a little about routed events
Example. if PreviewMouseLeftButtonUp does not work.
<StackPanel Mouse.MouseUp="MouseButtonUpEventHandler"/>
in the view model
public void MouseButtonUpEventHandler (RoutedEvent e)
{
//logic here to check if it's left mouse if it is then set visibility
}
}