I have a two-level hierarchy displayed in a WPF TreeView, but I only want the child nodes to be selectable - basically the top level nodes are for categorisation but shouldn't be selectable by themselves.
Can I achieve this?
Thanks...
Define styles for each type of items, like Bijington wrote. for non-selectable nodes set the Focusable-Property of the container (TreeViewItem for TreeViews) to false.
To do so you would need to override the style for treeview. Ideally you will have two types of treeview items one for your top-level nodes (im assuming folders) and another simply for the children, then you should be able to define how each item type in the tree behaves.
So create a style for each item type, then for the folder node simply change the trigger for is selected to do nothing.
I've written at attached property that will unselect a treeviewitem as soon as it's selected:
public class TreeViewItemHelper
{
public static bool GetIsSelectable(TreeViewItem obj)
{
return (bool)obj.GetValue(IsSelectableProperty);
}
public static void SetIsSelectable(TreeViewItem obj, bool value)
{
obj.SetValue(IsSelectableProperty, value);
}
public static readonly DependencyProperty IsSelectableProperty =
DependencyProperty.RegisterAttached("IsSelectable", typeof(bool), typeof(TreeViewItemHelper), new UIPropertyMetadata(true, IsSelectablePropertyChangedCallback));
private static void IsSelectablePropertyChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs args)
{
TreeViewItem i = (TreeViewItem) o;
i.Selected -= OnSelected;
if(!GetIsSelectable(i))
{
i.Selected += OnSelected;
}
}
private static void OnSelected(object sender, RoutedEventArgs args)
{
if(sender==args.Source)
{
TreeViewItem i = (TreeViewItem)sender;
i.IsSelected = false;
}
}
}
Unfortunately you still lose the old selection when you click on an unselectable item :(
You can make only the child nodes selectable by using a trigger in a style, assuming all child nodes have no child nodes, like so:
<TreeView Name="MyTreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Style.Triggers>
<Trigger Property="HasItems" Value="true">
<Setter Property="Focusable" Value="False" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
Setting Focusable to false on a father TreeViewItems also prevents them from being selected.
Related
I have created an UserControl which is loaded in a View (Window) in WPF. In my user control I have put a TextBox. I am unable to set focus on this text box when my view loads. I have tried following but nothing works for me:
FocusManager.FocusedElement="{Binding ElementName=PwdBox}"
I have created a FocusExtension to set focus on control.
Please help.
Another option that you have is to create a bool IsFocusedproperty in your view model. Then you can add a DataTrigger to set the focus when this property is true:
In a Resources section:
<Style x:Key="SelectedTextBoxStyle" TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsFocused}" Value="True">
<Setter Property="FocusManager.FocusedElement"
Value="{Binding RelativeSource={RelativeSource Self}}" />
</DataTrigger>
</Style.Triggers>
</Style>
...
<TextBox Style="{StaticResource SelectedTextBoxStyle}" ... />
Note that at times, you may need to set it to false first to get it to focus (only when it is already true):
IsFocused = false;
IsFocused = true;
This is similar to Sheridan's answer but does not require focus to be set to the control first. It fires as soon as the control is made visible and is based on the parent grid rather than the textbox itself.
In the 'Resources' section:
<Style x:Key="FocusTextBox" TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=textBoxName, Path=IsVisible}" Value="True">
<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=textBoxName}"/>
</DataTrigger>
</Style.Triggers>
</Style>
In my grid definition:
<Grid Style="{StaticResource FocusTextBox}" />
Register the Loaded-Event of your UserControl and set the Focus on your PwdBox by calling Focus() when your UserControl is loaded.
public class MyUserControl : UserControl{
public MyUserControl(){
this.Loaded += Loaded;
}
public void Loaded(object sender, RoutedEventArgs e){
PwdBox.Focus();
// or FocusManager.FocusedElement = PwdBox;
}
}
Keyboard focus will be set when the FocusManager.FocusedElement property is set. Since the property is set when an element is initialized, this is often useful for setting initial focus.
However this is not quite the same thing as setting focus on load. If it is unloaded and reloaded, for example, the keyboard focus will not move the second time. The actual intended purpose of the FocusedElement property is for temporary focus scopes (for example, when a menu is opened, the FocusedElement of the window is kept separate from the keyboard focus because the menu is a separate focus scope -- and keyboard focus returns to the FocusedElement when the menu is closed). If you set FocusedElement on a Window, it will not persist -- since a Window is a focus scope, it will automatically update its FocusedElement whenever you move keyboard focus within it.
To set focus on the Loaded event (without using code-behind), this attached property should work for you:
public static class FocusExtensions {
public static readonly DependencyProperty LoadedFocusedElementProperty =
DependencyProperty.RegisterAttached("LoadedFocusedElement", typeof(IInputElement), typeof(FocusExtension),
new PropertyMetadata(OnLoadedFocusedElementChanged));
public static IInputElement GetLoadedFocusedElement(DependencyObject element) {
return (IInputElement)element.GetValue(LoadedFocusedElementProperty);
}
public static void SetLoadedFocusedElement(DependencyObject element, bool value) {
element.SetValue(LoadedFocusedElementProperty, value);
}
private static void OnLoadedFocusedElementChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) {
var element = (FrameworkElement)obj;
var oldFocusedElement = (IInputElement)e.OldValue;
if (oldFocusedElement != null) {
element.Loaded -= LoadedFocusedElement_Loaded;
}
var newFocusedElement = (IInputElement)e.NewValue;
if (newFocusedElement != null) {
element.Loaded += LoadedFocusedElement_Loaded;
}
}
private static void LoadedFocusedElement_Loaded(object sender, RoutedEventArgs e) {
var element = (FrameworkElement)sender;
var focusedElement = GetLoadedFocusedElement(element);
focusedElement.Focus();
}
}
The usage is the same as FocusManager.FocusedElement, i.e.:
local:FocusExtensions.LoadedFocusedElement="{Binding ElementName=PwdBox}"
It worked for me to simply add this attribute to the opening UserControl tag in my XAML:
FocusManager.FocusedElement="{Binding ElementName=DisplayName, Mode=OneTime}"
Where DisplayName is the name of the textbox I want to receive focus.
What i use in my authentication manager:
private void SelectLogicalControl()
{
if (string.IsNullOrEmpty(TextboxUsername.Text))
TextboxUsername.Focus();
else
{
TextboxPassword.SelectAll();
TextboxPassword.Focus();
}
}
If no username is set, focus on the username-textbox; otherwise the (select all) passwordbox. This is in the codebehind-file, so not viewmodel ;)
On load event set the keyboard focus :
Keyboard.Focus(control);
Is there a way in XAML to call a function when the TreeViewItem property IsExpanded changes?
I believe the not so good alternative would be to loop through all TreeViewItems and do an item.IsExpanded += handler call if I understand things correctly.
Or I could check for clicks on the expander element I guess.
What I'm doing is persisting the expand/collapse state of the tree. Please answer the first question before suggesting alternative ways to persist this just to edify me on properties and xaml.
Building on Joel's answer, you can use EventSetters in the TreeViewItem Style which refer to event handlers in your code-behind:
<TreeView ... >
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem" >
<EventSetter Event="TreeViewItem.Expanded" Handler="OnTreeExpanded" />
<EventSetter Event="TreeViewItem.Collapsed" Handler="OnTreeCollapsed" />
</Style>
</TreeView.ItemContainerStyle>
...
Code-behind - normal event handlers:
private void OnTreeExpanded(object sender, RoutedEventArgs e)
{
var tvi = (TreeViewItem)sender;
...
e.Handled = true;
}
private void OnTreeCollapsed(object sender, RoutedEventArgs e)
{
var tvi = (TreeViewItem)sender;
...
e.Handled = true;
}
Note: Make sure you set e.Handled = true in the event handlers, or else you'll get events from all parents of the current TreeViewItem as well..
I would bind the IsExpanded property of the TreeViewItem to my model using something like:
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
<Setter Property="IsExpanded" Value="{Binding IsExpanded}" />
</Style>
</TreeView.ItemContainerStyle>
Then I can run thru the model and get the value for IsExpanded and save it. Additionally, when restoring, simply set the IsExpanded property.
Since you need to call other code when changed, implement IsExpanded like so:
private bool _IsExpanded;
public bool IsExpanded
{
get { return _IsExpanded; }
set
{
if (_IsExpanded == value) return;
_IsExpanded = value;
NotifyPropertyChanged( "IsExpanded" );//or however you need to do it
CallSomeOtherFunc();//this is the code that you need to be called when changed.
}
}
How do I do so that it is possible to expand/collaps groups in the TreeView simply by clicking on the text, instead of clicking the arrow to the left.
You should create style for your Tree Item with next setter:
<Style x:Key="TreeItemStyle"
TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded"
Value="{Binding Path=IsExpanded, Mode=TwoWay}"/>
</Style>
Then add to you group view data class observable property named IsExpanded:
private bool _isExpanded;
public bool IsExpanded
{
get
{
return this._isExpanded;
}
set
{
if (this._isExpanded != value)
{
this._isExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
}
}
Then intercept hyper link click event and set IsExpanded as true:
private void Hyperlink_Click(object sender, RoutedEventArgs e)
{
var dc = ((Hyperlink)sender).DataContext;
if (dc is GroupViewData)
{
((GroupViewData)dc).IsExpanded = true;
}
}
Of course, the best way is to use commands instead of click handlers, but I don't know composition of your presentation model so can't provide proper solution. I just must say that in our projects with alike requirements we successfully avoid any view code behind. God bless WPF!
We're looking for a way to base a WPF trigger in XAML on whether or not we're in a drag-drop operation. Depending on if we are or not, we want different hovering behaviors which is why this is needed.
The only way I can think of is to handle the drag start and end events and manually track the state, but that requires a code-behind, not pure XAML. Plus it seems like complete overkill, especially since we'd have to do it on every potential drop target which is a real pain.
So is there an easy way to say 'Hey... I'm in a drag-drop operation so make this trigger active' or am I out of luck here?
Update
To clarify what we are trying to do, currently in pure XAML, you can create a style, then set a style trigger to examine the IsMouseOver property to say, draw a background highlight. Well, we want to do this, but we want to say if 'IsMouseOver' is true and if IsDragging = true then apply this trigger.
I've just had this problem, my solution consists of using an attached property that supplies the missing IsDragging:
Define the attached Property
public static readonly DependencyProperty IsDraggingProperty =
DependencyProperty.RegisterAttached
(
"IsDragging",
typeof(bool),
typeof(ClassContainingThisProperty),
new UIPropertyMetadata(false)
);
public static bool GetIsDragging(DependencyObject source)
{
return (bool)source.GetValue(IsDraggingProperty);
}
public static void SetIsDragging(DependencyObject target, bool value)
{
target.SetValue(IsDraggingProperty, value);
}
Create this extension methods to help you set the Property
public static TParent FindParent<TParent>(this DependencyObject child) where TParent : DependencyObject
{
DependencyObject current = child;
while(current != null && !(current is TParent))
{
current = VisualTreeHelper.GetParent(current);
}
return current as TParent;
}
public static void SetParentValue<TParent>(this DependencyObject child, DependencyProperty property, object value) where TParent : DependencyObject
{
TParent parent = child.FindParent<TParent>();
if(parent != null)
{
parent.SetValue(property, value);
}
}
Handle DragDrop events according to the Control used (e.g. ListView), to set the attached property on the elements.
private void OnDragEnter(object sender, DragEventArgs e)
{
DependencyObject source = e.OriginalSource as DependencyObject;
if (source != null)
{
source.SetParentValue<ListViewItem>(ClassContainingTheProperty.IsDraggingProperty, true);
}
}
private void OnDragLeave(object sender, DragEventArgs e)
{
DependencyObject source = e.OriginalSource as DependencyObject;
if(source != null)
{
source.SetParentValue<ListViewItem>(ClassContainingTheProperty.IsDraggingProperty, false);
}
}
private void OnDrop(object sender, DragEventArgs e)
{
DependencyObject source = e.OriginalSource as DependencyObject;
if(source != null)
{
source.SetParentValue<ListViewItem>(ClassContainingTheProperty.IsDraggingProperty, false);
}
}
Use the Property in your Trigger
<ControlTemplate TargetType="{x:Type ListViewItem}">
<ControlTemplate.Triggers>
<Trigger Property="namespace:ClassContainingTheProperty.IsDragging" Value="True">
<Setter Property="Background" Value="White" />
<Setter Property="BorderBrush" Value="Black" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
I've only ever seen drag-drop implemented with event handlers, so I think you're out of luck. I suggest you create your own dependency property to indicate a drag drop in progress.
if it is a real DragDrop event just watch for the DragOver event...
<EventTrigger RoutedEvent="DragOver">
<BeginStoryboard>
<Storyboard Storyboard.TargetProperty="Fill.Color">
<ColorAnimation From="White" To="Black" Duration="0:0:1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
I have the same command that I want to use for two controls on a dialog type window. As potentially interesting background, I'm using Josh Smith's ViewModel / RelayCommand ideas, since I am new to WPF and it's the first thing I've seen that I can actually understand from a big picture point of view.
So the command is a property of a ViewModel, and with the Button's built-in support, it is trivial and painless to bind to the command in the XAML:
<Button ... Command="{Binding Path=PickCommand}" Content="_Ok"></Button>
Now in a ListView, the only way I have gotten to use the same command hooked up to trigger on a double click is by using an event handler:
<ListView ...
ItemsSource="{Binding Path=AvailableProjects}"
SelectedItem="{Binding Path=SelectedProject, Mode=TwoWay}"
MouseDoubleClick="OnProjectListingMouseDoubleClick"
>
private void OnProjectListingMouseDoubleClick(object sender, MouseButtonEventArgs e) {
var vm = (ProjectSelectionViewModel) DataContext;
vm.Pick(); // execute the pick command
}
Is there a way to do this by binding the way the button does it?
Cheers,
Berryl
<------- implementation - is there a better way? --->
Your SelctionBehavior class was spot on, but I was confused at your xaml code. By setting the "Style" on the listViewItem I was getting the children of the DataContext where the command I want to execute lives. So I attached the behavior to the ListView itself:
<ListView ...Style="{StaticResource _attachedPickCommand}" >
And put the style in a resource dictionary:
<Style x:Key="_attachedPickCommand" TargetType="ListView">
<Setter Property="behaviors:SelectionBehavior.DoubleClickCommand" Value="{Binding Path=PickCommand}" />
</Style>
It works! But it 'feels' awkward setting the style property of the list view. Is this just because I am not comfortable with style as more than something visual in wpf or is there a better way to do this?
Cheers, and thanks!
Berryl
Yes there is! You can use attached behaviors and bind the command to that behavior.
public class SelectionBehavior {
public static readonly DependencyProperty CommandParameterProperty=
DependencyProperty.RegisterAttached("CommandParameter", typeof(object), typeof(SelectionBehavior));
public static readonly DependencyProperty DoubleClickCommandProperty=
DependencyProperty.RegisterAttached("DoubleClickCommand", typeof(ICommand), typeof(SelectionBehavior),
new PropertyMetadata(OnDoubleClickAttached));
private static void OnDoubleClickAttached(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var fe=(FrameworkElement)d;
if(e.NewValue!=null && e.OldValue==null) {
fe.PreviewMouseDown+=fe_MouseDown;
} else if(e.NewValue==null && e.OldValue!=null) {
fe.PreviewMouseDown-=fe_MouseDown;
}
}
private static void fe_MouseDown(object sender, MouseButtonEventArgs e) {
if(e.ClickCount==2) {
var dep=(FrameworkElement)sender;
var command=GetDoubleClickCommand(dep);
if(command!=null) {
var param=GetCommandParameter(dep);
command.Execute(param);
}
}
}
public static ICommand GetDoubleClickCommand(FrameworkElement element) {
return (ICommand)element.GetValue(DoubleClickCommandProperty);
}
public static void SetDoubleClickCommand(FrameworkElement element, ICommand value) {
element.SetValue(DoubleClickCommandProperty, value);
}
public static object GetCommandParameter(DependencyObject element) {
return element.GetValue(CommandParameterProperty);
}
public static void SetCommandParameter(DependencyObject element, object value) {
element.SetValue(CommandParameterProperty, value);
}
}
and in the xaml you would need to set a style for a ListViewItem which represents your data in the ListView. Example
<ListView>
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="local:SelectionBehavior.DoubleClickCommand" Value="{Binding Path=DataContext.PickCommand}"/>
<Setter Property="local:SelectionBehavior.CommandParameter" Value="{Binding Path=DataContext}"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
Here is some more information about the Attached Behavior pattern