I am using a WPF treeview, when i click on a node\item once it gets selected. When the user clicks on the selected node the second time i want this node\item to get deselected i.e. i should be able to get the event. IsSelected is not called if i click on the selected node\item that is already selected. How do i get it to work?
<TreeView Grid.Column="0" Grid.Row="1" ItemsSource="{Binding source}" Name="mytreeview">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding displaytext}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
and in my view model i have
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
if (value != _isSelected)
{
_isSelected = value;
if (_isSelected)
{
//my logic
}
this.OnPropertyChanged("IsSelected");
}
}
}
if (value != _isSelected)
Assuming that the UI is even trying to set something, that line is blocking your toggle logic. Something like this should fix at least that part.
set
{
if (value != _isSelected)
{
_isSelected = value;
this.OnPropertyChanged("IsSelected");
}
else if(_isSelected)
{
IsSelected = false;
}
}
Otherwise the UI is checking the selection before setting the value and you'll need to handle it through some other user interaction like handling deselection on click.
I know this is a bit late but I've recently had the same requirement (i.e. unselecting a selected TreeViewItem on the second click) and I solved it by declaring an event handler for the 'MouseLeftButtonUp' event in a 'Style' entry for the ItemContainerStyle of the TreeView as follows:
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="MouseLeftButtonUp" Handler="TreeViewItem_MouseLeftButtonUp"/>
</Style>
</TreeView.ItemContainerStyle>
The event handler in the code behind was as follows:
private TreeViewItem prevTVI;
private void TreeViewItem_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
TreeViewItem tvi = (TreeViewItem)sender;
if (tvi == this.prevTVI)
{
this.prevTVI = null;
if (tvi.IsSelected)
tvi.IsSelected = false;
}
else
this.prevTVI = tvi;
e.Handled = true;
}
Now, I would like to ask if anyone thinks this approach breaks the MVVM pattern? I personally don't think so as the event handler is only concerned with the View and its objects not anything else but I would like to hear what others have to say, especially if someone has an alternative.
The IsSelected property is only changed when you select a new item. Clicking on the same item twice will normally have no effect. You would need to register the MouseDown event on the TreeView, and then force the item to be deselected in the code-behind.
Related
I have a DatagridTemplateColumn with Combobox which is editable. Depending on another property value, this will be either a Text box or combo box. So, if Modelist is null, isEditable will be true making it more like a text box and if list is initialized isEditable will be false with dropdown values. So as user edit, the SelectedModeValue gets updated.
My problem is, say when isEditable is false, the user chooses one item from the dropdown, the SelectedModeValue will have that chosen value, say "mode1". Now, on changing someother property( say 'ModeType' from code below), my ModeList will go null and isEditable becomes True as there are no list. TextBox is displayed correctly with no default value, but behind the SelectedModeValue still has value "mode1". it needs to be resets to be string.empty. Hence, i wanted to know if there is any event when isEditable changes value on the combo box. If thats possible, with that event can you show me how i can reset SelectedModeValue depending on the isEditable change event?
Or any other alternative way ?
<DataGrid.Columns>
<DataGridTemplateColumn Header="ModeList">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding ModeList}" SelectionChanged="ValueChanged"
Text="{Binding SelectedModeValue, Mode=TwoWay, UpdateSourceTrigger=LostFocus, ValidatesOnNotifyDataErrors=True,
ValidatesOnDataErrors=True,NotifyOnValidationError=True, ValidatesOnExceptions=True}">
<ComboBox.Style>
<Style TargetType="ComboBox">
<Setter Property="IsEditable" Value="False"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ModeList}" Value="{x:Null}">
<Setter Property="IsEditable" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
...<! other properties -->
</DataGrid.Columns>
public List<string> ModeList
{
get { return modeList; }
set
{
modeList= value;
OnPropertyChanged("ModeList");
}
}
public ModeTypeEnum ModeType
{
get { return modeType; }
set
{
modeType = value;
OnPropertyChanged("ModeType");
}
}
public enum ModeTypeEnum
{
slow= 0,
Fast = 1,
Forward = 2,
}
You should handle this in the setter of the property in the data object:
public List<string> ModeList
{
get { return modeList; }
set
{
modeList = value;
OnPropertyChanged("ModeList");
if (modeList == null)
SelectedModeValue = string.Empty:
}
}
Implementing this kind of logic in the view is a bad idea, especially if you adopt to the MVVM design pattern.
I the following code
<DataGrid.RowHeaderTemplate >
<DataTemplate>
<CheckBox x:Name="SelectedItemCheckBox"
Margin="5 0 0 0"
IsChecked="{Binding Path=IsSelected,
Mode=TwoWay,
RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type DataGridRow}}}">
</CheckBox>
</DataTemplate>
</DataGrid.RowHeaderTemplate>
or
<DataGrid.RowHeaderStyle>
<Style TargetType="{x:Type DataGridRowHeader}">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridRowHeader}">
<CheckBox x:Name="SelectedItemCheckBox"
Margin="5 0 0 0"
IsChecked="{Binding Path=IsSelected,
Mode=TwoWay,
RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type DataGridRow}}}">
</CheckBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.RowHeaderStyle>
How can I access the SelectedItemCheckBox from code behind when row is selected ?
What I have so far:
private CheckBox GetCheckbox(int index)
{
DataGridRow row = (DataGridRow)MyGrid.ItemContainerGenerator.ContainerFromIndex(index);
//how do I get to that checkbox here ?
}
The ItemSource of MyGrid is set in code behind, normally I would access the cell by accessing MyGrid.Columns[] however this is a row header and it's not part of Columns[].
Please note that there are many rows with this checkbox defined depending the ItemSource size.
Also I wold like to know if there is a way of accessing the checkbox without changing the xaml and using it as it is.
If you want to access the row header's checkbox in your code-behind (and not use binding), you can "travel" the visual tree of your selected DataGridRow to find the header.
Add SelectionChanged event handler to the DataGrid:
<DataGrid x:Name="Grid" Loaded="Grid_Loaded" SelectionChanged="Grid_SelectionChanged">
Then in code-behind:
Get the selected row
Use VisualTreeHelper to find the header's checkbox
Do your magic
private void Grid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var item = (DataGridRow)Grid.ItemContainerGenerator.ContainerFromItem(e.AddedItems[0]);
var control = FindChild<CheckBox>(item, "SelectedItemCheckBox");
control.IsChecked = true;
}
For FindChild, there's multiple options available in here: How can I find WPF controls by name or type?
I used the following in this example: How can I find WPF controls by name or type?
public static T FindChild<T>(DependencyObject depObj, string childName)
where T : DependencyObject
{
// Confirm obj is valid.
if (depObj == null) return null;
// success case
if (depObj is T && ((FrameworkElement)depObj).Name == childName)
return depObj as T;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
//DFS
T obj = FindChild<T>(child, childName);
if (obj != null)
return obj;
}
return null;
}
I have this code in my xaml which says to color my button when I hover my mouse and click my mouse over the button.
<Border x:Class="DatasetGrid.RowHeaderButton"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" MinWidth="30" Width="Auto">
<Border.Resources>
<SolidColorBrush x:Key="ButtOverBrush" Color="#53C3D5" Opacity="0.2"></SolidColorBrush>
<SolidColorBrush x:Key="ButtPressedBrush" Color="#53C3D5" Opacity="0.5"></SolidColorBrush>
</Border.Resources>
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Transparent"></Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource ButtOverBrush}"></Setter>
</Trigger>
<DataTrigger Binding="{Binding IsMouseDown, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Background" Value="{StaticResource ButtPressedBrush}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
This works all well and good, but I find that as soon as I change the Background color in code behind, the above MouseOver and MouseDown triggers don't fire anymore.
RowHeaderButton rhb = RowHeadersColumn.VisibleRowHeaders[cell.CellInfo.RowIndex];
rhb.Background = new SolidColorBrush(Color.FromArgb(100, 83, 195, 213));
I'm quite new to WPF so I'm not sure what's going wrong.
Edit:
So to give some more information, my control above is a RowHeaderButton, i.e the row header to a grid. Each row in the grid has it's own row header button. So when the user hovers over or clicks it, it should change from white to the specified SolidColorBrush above.
In the code behind of another control, DataGrid.xaml.cs, I have the below code (simplified) which will change the color of the row header when when a cell in the same row of the grid is selected or not.
void UpdateSelectedCells() {
foreach (Cell cell in VisibleColumns.SelectMany(c => c.VisibleCells))
{
int cellRowIndex = cell.CellInfo.RowIndex;
cell.IsSelected = SelectedCells.Contains(cell.CellInfo);
foreach (RowHeaderButton rhb in RowHeadersColumn.VisibleRowHeaders)
{
int rowHeaderIndex = Convert.ToInt16(rhb._default.Text) - 1;
if (cellRowIndex == rowHeaderIndex)
{
if (cell.IsSelected)
{
rhb.Background = new SolidColorBrush(Color.FromArgb(100, 83, 195, 213));
}
else
{
bool rowselected = false;
//need to check if any other cell in the row is selected, if not then color row header white
foreach (CellInfo celll in SelectedCells)
{
if (celll.RowIndex == cellRowIndex)
{
rowselected = true;
break;
}
}
if (rowselected == false)
rhb.Background = Brushes.White;
}
}
}
}
}
I don't have a ViewModel for this.
The triggers are firing, but their setters are being overridden.
This is due to Dependency Property Value Precendence. If the Background property is set programmatically or as an attribute in the XAML, that value will override anything value any style setter gives it. In general, this is desirable behavior: You want to be able to override what the style does on an individual control.
The solution to this is to do all of your background brush changes in style triggers. Your code behind must have some reason for setting the background brush when it does. Whatever that is, find a way to do it with a trigger. Set a property on the viewmodel and write a trigger on that property.
If you need help translating that high level abstraction into your own code, please share enough code for me to understand why and where the codebehind is setting the Background, and what (if anything) you have for a viewmodel.
I solved the issue by creating a new Dependancy Property and binding it to a data trigger.
public bool IsCellSelected
{
get { return (bool)GetValue(IsCellSelectedProperty); }
set { SetValue(IsCellSelectedProperty, value); }
}
public static readonly DependencyProperty IsCellSelectedProperty =
DependencyProperty.Register("IsCellSelected", typeof(bool), typeof(RowHeaderButton), new PropertyMetadata(null));
In my xaml I have:
<DataTrigger Binding="{Binding IsCellSelected, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Background" Value="{StaticResource ButtPressedBrush}"></Setter>
</DataTrigger>
And in my code behind I set the value using:
RowHeaderButton rhb = RowHeadersColumn.VisibleRowHeaders[cell.CellInfo.RowIndex];
rhb.IsCellSelected = true; //or false
Now my button hover and button click events are not overridden.
Is there any way to achieve something like office undo drop down (image bellow) ?
I mean, i want to highlight previous item when user mouse over item other than first ?
I tried some control from FluentRibbon but so far without luck..
In most cases designing a control like this is done in Blend. However, if you don't know how to use Blend, you can still achieve the similar results with just XAML and the code-behind, but you have to do more work.
We start by creating a class called CustomListBoxItem which inherits from ListBoxItem. Then, we define a dependency property, which is used for highlighting items in the listbox:
public class CustomListBoxItem : ListBoxItem
{
public static readonly DependencyProperty IsVirtuallySelectedProperty =
DependencyProperty.Register("IsVirtuallySelected", typeof(bool),
typeof(CustomListBoxItem),
new PropertyMetadata(false));
public CustomListBoxItem() : base()
{ }
public bool IsVirtuallySelected
{
get { return (bool)GetValue(IsVirtuallySelectedProperty); }
set { SetValue(IsVirtuallySelectedProperty, value); }
}
}
Then we add a listbox and define a style for it in XAML:
<ListBox Name="listBox" MouseMove="listBox_MouseMove" SelectionChanged="listBox_SelectionChanged">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type local:CustomListBoxItem}">
<Style.Triggers>
<Trigger Property="IsVirtuallySelected" Value="true">
<Setter Property="Background" Value="SkyBlue"/>
</Trigger>
<Trigger Property="IsVirtuallySelected" Value="false">
<Setter Property="Background" Value="White"/>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
where local is the namespace in which CustomListBoxItem is defined. This is all we need for the XAML part, the real magic happens in the code behind.
The listBox_MouseMove event handler looks like this:
private void listBox_MouseMove(object sender, MouseEventArgs e)
{
bool itemFound = false;
for (int i = 0; i < listBox.Items.Count; i++)
{
var currentItem = listBox.ItemContainerGenerator.ContainerFromIndex(i) as CustomListBoxItem;
if (currentItem == null)
continue;
// Check whether the cursor is on an item or not.
if (IsMouseOverItem(currentItem, e.GetPosition((IInputElement)currentItem)))
{
// Unselect all items before selecting the new group
listBox.Items.Cast<CustomListBoxItem>().ToList().ForEach(x => x.IsVirtuallySelected = false);
// Select the current item and the ones above it
for (int j = 0; j <= listBox.Items.IndexOf(currentItem); j++)
{
((CustomListBoxItem)listBox.Items[j]).IsVirtuallySelected = true;
}
itemFound = true;
break;
}
}
// If the item wasn't found for the mouse point, it means the pointer is not over any item, so unselect all.
if (!itemFound)
{
listBox.Items.Cast<CustomListBoxItem>().ToList().ForEach(x => x.IsVirtuallySelected = false);
}
}
And the IsMouseOverItem helper method, which is used to determine if the cursor is on an item, is defined like this:
private bool IsMouseOverItem(Visual item, Point mouseOverPoint)
{
Rect currentDescendantBounds = VisualTreeHelper.GetDescendantBounds(item);
return currentDescendantBounds.Contains(mouseOverPoint);
}
And finally the listBox_SelectedChanged event handler which acts as the click handler for the ListBox:
private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Get all the virtually selected items
List<CustomListBoxItem> selectedItems =
listBox.Items.Cast<CustomListBoxItem>().Where(x => x.IsVirtuallySelected).ToList();
if (selectedItems == null || !selectedItems.Any())
return;
// Do something with the selected items
DoCoolStuffWithSelectedItems();
// Unselsect all.
listBox.Items.Cast<CustomListBoxItem>().ToList().ForEach(x => x.IsVirtuallySelected = false);
listBox.UnselectAll();
}
And boom, we're done. We can now add some items to the ListBox in the class constructor:
public MainWindow()
{
InitializeComponent();
listBox.Items.Add(new CustomListBoxItem { Content = "hello world!" });
listBox.Items.Add(new CustomListBoxItem { Content = "wpf is cool" });
listBox.Items.Add(new CustomListBoxItem { Content = "today is tuesday..." });
listBox.Items.Add(new CustomListBoxItem { Content = "I like coffee" });
}
Note that I used a random color as the highlight color in XAML. Feel free to change it and give it a try.
Guess you need something like this:
<ControlTemplate TargetType="ListBoxItem">
<TextBlock Text="{Binding LastOperation}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger>
<DataTrigger.Binding>
<MultiBinding>
<Binding Path="MouseOverIndex"/>
<Binding Path="CurrentIndex"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Foreground" Value="Gold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</ControlTemplate>
I have a textbox containing a decimal value on my interface that I want to clear whenever the user selects it.
However, if the user doesn't make any changes and selects another interface element I need the text to revert to whatever it was previous to the clear.
So far I have the the following style:
<Style x:Key="CustomTextBoxStyle" TargetType="{x:Type TextBox}">
<Setter Property="Text" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}"/>
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Text" Value="{x:Null}" />
</Trigger>
</Style.Triggers>
</Style>
And then the following to use the style:
<TextBox Style="{DynamicResource CustomTextBoxStyle}"
Tag="{Binding myDecimalValue, StringFormat=#.###}"
TabIndex="1" />
However, in this scenario the value reverts back to what it was even when the user enters a new value.
Can anyone tell me the best way to go about achieving this?
Thanks,
The solution here is not to hide the text but to store it in a variable for use later. In C# the code would be something like:
string _originalValue;
public OnFocus(){
_originalValue = TextBox.Text;
TextBox.Text = "";
}
public LostFocus(){
if(TextBox.Text == "")
TextBox.Text = _originalValue;
}
You could set the forground colour to transparent to hide the text if that's appropriate.
If you actually want to delete the text you should do what Ryan Amies is suggesting on the viewmodel which you should be able to get through the datacontext.
Thanks for the help, but I was able to achieve what I was looking for and adhere to MVVM principles by using the AttachedProperty described at the following:
https://stackoverflow.com/a/7972361/1466960
This allowed me to bind the IsFocused property to a value in my view model and proceed in a similar fashion to the one described by Ryan Amies.
View Model:
bool isFocused = false;
double original;
public bool IsFocused
{
get
{
return isFocused;
}
set
{
isFocused = value;
if (isFocused)
{
original = current;
current = "";
}
else
{
if (HundredPercentLine == "")
current = original;
}
OnPropertyChanged(new PropertyChangedEventArgs("IsFocused"));
}
}