WPF How correct modify view from ICommand with MVVM - c#

I need to load ComboBox ItemsSource before the control is expanded. And if loading failed I want set border brush color to red and show tooltip with error. Can I do this in ICommand.Execute method or should use something like ValidationRule?
Code:
class ViewModel : INotifyPropertyChanged
{
public string Server { get {...} set {...} }
public ObservableCollection<string> ServerCollection { get; }
public ICommand LoadServerListCommand { get; }
protected ConnectionViewModel()
{
ServerCollection = new ObservableCollection<string>();
LoadServerListCommand = new DelegateCommand( LoadServerList );
}
private void LoadServerList( object param )
{
var comboBox = param as ComboBox;
if ( comboBox != null && !comboBox.IsDropDownOpen )
{
try
{
ServerCollection.Clear();
///... Load();
comboBox.BorderBrush = //default;
comboBox.ToolTip = null;
}
catch( InvalidOperationException ex )
{
comboBox.BorderBrush = //red;
comboBox.ToolTip = new ToolTip()
{
Content = ex.Message
};
}
}
}
}
XAML:
<ComboBox x:Name="cbServer" ItemsSource="{Binding ServerCollection}"
SelectedItem="{Binding Server, Mode=TwoWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewMouseDown">
<i:InvokeCommandAction Command="{Binding Path=LoadServerListCommand}"
CommandParameter="{Binding ElementName=cbServer}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>

You could add a property to the view model that indicates whether the loading was successful and bind to this property and change the appropriate properties of the ComboBox using a Style and a DataTrigger in the view.
Setting the BorderBrush of a ComboBox requires you to define a custom template for its ToggleButton though: https://blog.magnusmontin.net/2014/04/30/changing-the-background-colour-of-a-combobox-in-wpf-on-windows-8/
It will be easier to wrap the ComboBox in a Border element:
<Border BorderThickness="1">
<ComboBox x:Name="cbServer" ItemsSource="{Binding ServerCollection}"
SelectedItem="{Binding Server, Mode=TwoWay}">
<ComboBox.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.WindowFrameBrushKey}" Color="Red"/>
</ComboBox.Resources>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding Path=LoadServerListCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<ComboBox.Style>
<Style TargetType="ComboBox">
<Setter Property="ToolTip" Value="{Binding Error}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Error.Length, FallbackValue=0}" Value="0">
<Setter Property="ToolTip" Value="{x:Null}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
<Border.Style>
<Style TargetType="Border">
<Setter Property="BorderBrush" Value="Red" />
<Style.Triggers>
<DataTrigger Binding="{Binding Error.Length, FallbackValue=0}" Value="0">
<Setter Property="BorderBrush" Value="{x:Null}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
View Model:
private void LoadServerList(object parameter)
{
try
{
//throw new InvalidOperationException("test");
ServerCollection.Clear();
///... Load();
Error = string.Empty;
}
catch (InvalidOperationException ex)
{
Error = ex.Message;
}
}
private string _error;
public string Error
{
get { return _error; }
set { _error = value; NotifyPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}

Related

how to focus on the keyboard on a textbox in WPF listview after item.refresh

I have a WPF window with a listview with textboxes in it, and I want to enable users to switch between textboxes with the TAB key. I created a function that does that, but every time the textbox loses focus, I refresh the listview and so the listview itself gets focused.
I know the problem occurs because of the refresh event because commenting that part will result in the correct element (the textbox) being focused.
I have tried many alternative solutions but none works (prevent item losing focus when refreshing list view in c#; Focus on TextBox after ListView 'SelectionChanged' Event).
It seems there is something problematic in focusing on an that element after the listview is refreshed.
I have tried remembering the index of the item and then focusing on it, after the TAB key was pressed. tried also remembering the focused control and then focusing on it after the refresh.
private void RTB_Reference_LostFocus(object sender, RoutedEventArgs e)
{
int Index = DetailsList.SelectedIndex;
Index = DetailsList.Items.IndexOf(DetailsList.SelectedItem);
try
{
//Get cell value by using sender Object
string inTime = ((System.Windows.Controls.TextBox)sender).Text;
DetailItem item = (DetailItem)DetailsList.Items[Index];
item.Reference = inTime;
UpdateExplanation(item);
}
catch (Exception)
{
}
}
private void RTB_Detail_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == Key.Tab)
{
e.Handled = true;
//System.Windows.MessageBox.Show("Tab");
int Idx = DetailsList.SelectedIndex;
System.Windows.Controls.ListViewItem lvi = (System.Windows.Controls.ListViewItem)DetailsList.ItemContainerGenerator.ContainerFromItem(DetailsList.SelectedItem);
GUF.FocusItem(DetailsList, Idx, "RTB_Detail");
//IsLastKeyTAB = true;
}
//else
// IsLastKeyTAB = false;
}
private void UpdateExplanation(DetailItem item)
{
item.Explanation = GetExplanation(item.Reference, item.Detail);
IInputElement focusedControl = Keyboard.FocusedElement;
DetailsList.Items.Refresh();
focusedControl.Focus();
RefreshDetailsList(DetailsList, IsEnglish);
}
expected result is to keep focus on that textbox after the refresh. that does not occur...
EDIT 1
This is the listview xaml:
<ListView FlowDirection="RightToLeft" x:Name="DetailsList" VirtualizingStackPanel.IsVirtualizing="False"
HorizontalAlignment="Stretch" VerticalAlignment="Top"
HorizontalContentAlignment="Center" ScrollViewer.VerticalScrollBarVisibility="Visible" Padding="0 0 0 25"
AllowDrop="True"
ItemsSource="{Binding DetailItem}"
Loaded="ListView_Loaded"
Margin="26,157,23,0"
dd:DragDrop.IsDragSource="True"
dd:DragDrop.IsDropTarget="True"
dd:DragDrop.DropHandler="{Binding}" Height="599">
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="Margin" Value="4, 4, 4, 4"/>
<Setter Property="FontWeight" Value="DemiBold"/>
<Setter Property="Height" Value="22"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListViewItem}">
<Border BorderBrush="Transparent" BorderThickness="0" Background="{TemplateBinding Background}">
<GridViewRowPresenter HorizontalAlignment="Stretch" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Width="Auto" Margin="0" Content="{TemplateBinding Content}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<!-- <Setter Property="Background" Value="#6B54FF"/> -->
<Setter Property="Foreground" Value="#6B57FF"/>
<Setter Property="BorderThickness" Value="2" />
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="BorderThickness" Value="2" />
<Setter Property="BorderBrush" Value="#6B57FF"/>
<Setter Property="Foreground" Value="#6B57FF" />
</Trigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
<ListView.View>
<GridView>
<GridViewColumn Width="50" Header="סימון" DisplayMemberBinding="{Binding Mark}"/>
<!--
<GridViewColumn Width="30" >
<GridViewColumn.CellTemplate>
<DataTemplate >
<Button Style="{StaticResource PlusButtonStyle}" x:Name="buttonPlusDocument" Click="buttonPlusDocument_Click" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
-->
<GridViewColumn Header="הפניה במסמך" Width="150">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox x:Name="RTB_Reference" BorderBrush="#5f27ff" BorderThickness="1" KeyDown="RTB_Reference_KeyDown" HorizontalAlignment="Stretch" Height="20" Margin="0" Padding="0" FontSize="12" IsEnabled="True"
LostFocus="RTB_Reference_LostFocus" GotFocus="RTB_Reference_GotFocus">
<TextBox.Resources>
<Style TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="2"/>
<Setter Property="BorderBrush" Value="#5f27ff"/>
<Setter Property="BorderThickness" Value="1" />
</Style>
</TextBox.Resources>
</TextBox>
<!--DataContext="{Binding SelectedItem, ElementName=ListViewAppendixNameList, Mode=TwoWay}"-->
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="פרט" Width="150">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox x:Name="RTB_Detail" BorderBrush="#5f27ff" BorderThickness="1" HorizontalAlignment="Stretch" Height="20" Margin="0" Padding="0" FontSize="12" IsEnabled="True"
KeyDown="RTB_Detail_KeyDown" LostFocus="RTB_Detail_LostFocus" GotFocus="RTB_Detail_GotFocus">
<TextBox.Resources>
<Style TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="2"/>
<Setter Property="BorderBrush" Value="#5f27ff"/>
<Setter Property="BorderThickness" Value="1" />
</Style>
</TextBox.Resources>
</TextBox>
<!--DataContext="{Binding SelectedItem, ElementName=ListViewAppendixNameList, Mode=TwoWay}"-->
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="הסבר" Width="350" DisplayMemberBinding="{Binding Explanation}"/>
<GridViewColumn Width="30" >
<GridViewColumn.CellTemplate>
<DataTemplate >
<Button Style="{StaticResource DeleteButtonStyle}" x:Name="BT_DeleteDetail" Click="BT_DeleteDetail_Click" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
This is the details class:
public class DetailItem
{
public string Mark { get; set; }
public string Reference { get; set; }
public string Detail { get; set; }
public string Explanation { get; set; }
}
I refresh the listview so that the Explanation text will be updated.
I don't went through your whole code but only those parts that are relevant to your question. To simplify things, I will use the attached property TabNavigation and the data binding feature. The binding will automatically update the control values so that the refreshing of the ListView and the focus handling becomes redundant.
Remark: To shorten the XAML code I will only show the relevant code portions.
Tab navigation
Set the attached property KeyboardNavigation.TabNavigation on the ListView to Continue so that the focus will loop through the list:
<ListView x:Name="DetailsList"
KeyboardNavigation.TabNavigation="Continue"
...
</ListView>
You can control which elements can receive focus on Tab key pressed by setting the property IsTabStop on the elements. The default value is True. So set it to False on those elements you wish to exclude. It's useful to disable Tab focus on the ListViewItem itself so that the Tab key is only effective for the containing controls:
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="IsTabStop"
Value="False" />
...
</Style>
</ListView.ItemContainerStyle>
If you wish to exclude the Button as well, so that you only jump between the TextBox elements, set the Button's IsTabStopproperty to False as well:
<GridViewColumn Width="30">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button x:Name="BT_DeleteDetail"
IsTabStop="False"
...
/>
...
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
If you also wish to change the order the elements will receive focus on Tab key pressed, you can do it by setting the element's TabIndex property. A control with a lower tab index receives focus before a control with a higher index.
Binding the TextBox
To enable binding to a data model it is required that the data model (view model) implements INotifyPropertxChanged:
public class DetailItem : INotifyPropertyChanged
{
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
private string mark;
public string Mark
{
get => this.mark;
set
{
if (value == this.mark) return;
this.mark = value;
OnPropertyChanged();
}
}
private string reference;
public string Reference
{
get => this.reference;
set
{
if (value == this.reference) return;
this.reference = value;
OnPropertyChanged();
}
}
private string detail;
public string Detail
{
get => this.detail;
set
{
if (value == this.detail) return;
this.detail = value;
OnPropertyChanged();
}
}
private string explanation;
public string Explanation
{
get => this.explanation;
set
{
if (value == this.explanation) return;
this.explanation = value;
OnPropertyChanged();
}
}
}
In order access the view model from your DataTemplate set its DataType property to the type of the view model (DetailItem). Remember to always set the DataType of a DataTemplate. Then add the Binding to the TextBox. Note that the Mode (direction) of the binding is set to OneWayToSource. This limits the binding to set data from TextBoxto DetailItem only:
<GridViewColumn.CellTemplate>
<DataTemplate DataType="viewModels:DetailItem">
<TextBox x:Name="RTB_Reference"
Text="{Binding Reference, Mode=OneWayToSource}"
...
</TextBox>
...
</DataTemplate>
</GridViewColumn.CellTemplate>
<GridViewColumn.CellTemplate>
<DataTemplate DataType="viewModels:DetailItem">
<TextBox x:Name="RTB_Detail"
Text="{Binding Detail, Mode=OneWayToSource}"
...
</TextBox>
...
</DataTemplate>
</GridViewColumn.CellTemplate>
The final step is to update the DetailItem.Explanation property. To do this, we move the UpdateExplanation() method into the view model DetailItem. Since we use binding we can now get rid of all the ListView refresh and focus logic inside, so the method becomes smaller. Note that since we moved the method into the DetailItem class, we can also remove the parameter of the method:
private void UpdateExplanation()
{
this.Explanation = GetExplanation(this.Reference, this.Detail);
}
The UpdateExplanation() method is invoked directly from the setter of the Reference and Detail property, so that everytime these properties are changed the Explanation value will be updated. With the updated Reference and Detail setter the final version of the DetailItem class will then look as followed:
public class DetailItem : INotifyPropertyChanged
{
private void UpdateExplanation()
{
this.Explanation = GetExplanation(this.Reference, this.Detail);
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
private string mark;
public string Mark
{
get => this.mark;
set
{
if (value == this.mark) return;
this.mark = value;
OnPropertyChanged();
}
}
private string reference;
public string Reference
{
get => this.reference;
set
{
if (value == this.reference) return;
this.reference = value;
OnPropertyChanged();
UpdateExplanation();
}
}
private string detail;
public string Detail
{
get => this.detail;
set
{
if (value == this.detail) return;
this.detail = value;
OnPropertyChanged();
UpdateExplanation();
}
}
private string explanation;
public string Explanation
{
get => this.explanation;
set
{
if (value == this.explanation) return;
this.explanation = value;
OnPropertyChanged();
}
}
}
Try Below Code. Enjoy !!!
<Grid>
<ListView Margin="45.5,47.5,41.167,0" Name="lvtestListview" Height="188.333" VerticalAlignment="Top">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<!--<Setter Property="IsSelected" Value="{Binding IsGroovy}"/>-->
<Style.Triggers>
<Trigger Property="IsSelected" Value="true" >
<Setter Property="Foreground" Value="Blue" />
<Setter Property="Background" Value="LightGreen" />
</Trigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
<ListView.View>
<GridView>
<GridViewColumn Header="Item Code" Width="100">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border BorderBrush="Gray" BorderThickness=".5" Margin="-6,-3">
<TextBlock Text="{Binding ItemCode}" Margin="6,3"/>
</Border>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Item Name" Width="100">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border BorderBrush="Gray" BorderThickness=".5" Margin="-6,-3">
<TextBox Loaded="txtBarcode_Loaded" Text="{Binding ItemName}" Name="txtBarcode" Focusable="{Binding IsFocused}" Cursor="IBeam" />
</Border>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Next Item" Width="100">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border BorderBrush="Gray" BorderThickness=".5" Margin="-6,-3">
<TextBox TabIndex="3" Name="txtgvPL1" KeyDown="txtgvPL1_KeyDown" Text="{Binding PL1}" Cursor="IBeam"/>
</Border>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
code behind
public class ClassA : INotifyPropertyChanged
{
public string ItemCode { get; set; }
public string ItemName { get; set; }
private bool isFocused;
public event PropertyChangedEventHandler PropertyChanged;
public bool IsFocused
{
get
{
return isFocused;
}
set
{
if (value != isFocused)
{
isFocused = value;
RaisePropertyChanged("IsFocused");
}
}
}
private void RaisePropertyChanged(string propName)
{
PropertyChangedEventHandler eh = PropertyChanged;
if (eh != null)
{
eh(this, new PropertyChangedEventArgs(propName));
}
}
}
public partial class MainWindow : Window
{
List<ClassA> lstClassA = new List<ClassA>();
public MainWindow()
{
InitializeComponent();
BindGrid();
}
private void BindGrid()
{
ClassA ca = new ClassA();
ca.ItemCode = "A";
ca.IsFocused = true;
lstClassA.Add(ca);
lvtestListview.ItemsSource = lstClassA;
}
private void txtBarcode_Loaded(object sender, RoutedEventArgs e)
{
Keyboard.Focus(((TextBox)sender));
}
private void txtgvPL1_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter )
{
ClassA ca = new ClassA();
ca.ItemCode = "B";
ca.IsFocused = true;
lstClassA.Add(ca);
foreach (ClassA a in lstClassA)
{
if (a.ItemCode == "A")
{
a.IsFocused = false;
}
}
lvtestListview.ItemsSource = lstClassA;
lvtestListview.Items.Refresh();
}
}
}

WPF: how to auto show my ListView ToolTip when focus

So i have ListViewItem with my object:
public class ClipboardItem : INotifyPropertyChanged
{
private string _text { get; set; }
public string Text
{
get { return _text; }
set
{
_text = value;
NotifyPropertyChanged();
}
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
And ToolTip and i want when my ListViewItem IsSelected=True to show my ToolTip
This is my ListViewItem CellTemplate:
<TextBlock Text="{Binding Path=Text}"
Foreground="{Binding Path=Foreground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListViewItem}}}"
Grid.Column="1"
Margin="5,0,0,0">
<TextBlock.ToolTip>
<ToolTip Content="{Binding Path=Text}"
Placement="Left"
PlacementRectangle="0,0,0,0"
HorizontalOffset="10"
VerticalOffset="20"
HasDropShadow="false"/>
</TextBlock.ToolTip>
</TextBlock>
When i move over my ListViewItem with my Up & Down arrows i want to auto show my TooTip and not only when MouseIsOver
i also try to dso it in my ListViewItem style:
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsMouseOver}" Value="False" />
<Condition Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsSelected}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Silver"/>
<Setter Property="BorderThickness" Value="0.5"/>
<Setter Property="ToolTip" Value="{Binding Path=Text}"/>
</MultiDataTrigger>
Non of them works and i can see my ToolTip only when MouseIsOver
In general this is a really bad idea. ToolTips have a very specific behavior, which is why WPF doesn't expose it as conveniently as WinForms did with ToolTip.Show. The correct thing to use here would be an adorner.
That said, if you are absolutely adamant about forcibly showing a ToolTip manually then it can be done with a behavior, but you're going to have to fudge some of the functionality that's normally taken care of for you:
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace YourApp.Behaviors
{
public class ToolTipBehavior : Behavior<FrameworkElement>
{
private ToolTip CurrentToolTip;
public ListViewItem ListViewItem
{
get { return (ListViewItem)GetValue(ListViewItemProperty); }
set { SetValue(ListViewItemProperty, value); }
}
// Using a DependencyProperty as the backing store for ListViewItem. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ListViewItemProperty =
DependencyProperty.Register("ListViewItem", typeof(ListViewItem), typeof(ToolTipBehavior),
new PropertyMetadata(null, (d, e) => (d as ToolTipBehavior)?.OnListViewItemChanged(e)));
private void OnListViewItemChanged(DependencyPropertyChangedEventArgs e)
{
if (e.OldValue is ListViewItem)
(e.OldValue as ListViewItem).Selected -= ToolTipBehavior_Selected;
if (e.NewValue is ListViewItem)
(e.NewValue as ListViewItem).Selected += ToolTipBehavior_Selected;
}
private void ToolTipBehavior_Selected(object sender, RoutedEventArgs e)
{
if (e.Source != e.OriginalSource)
return;
if ((this.ListViewItem != null) && this.ListViewItem.IsSelected)
{
var tooltip = this.AssociatedObject.ToolTip as ToolTip;
if (tooltip != null)
{
if (this.CurrentToolTip != tooltip)
{
if (this.CurrentToolTip != null)
this.CurrentToolTip.Opened -= Tooltip_Opened;
this.CurrentToolTip = tooltip;
if (this.CurrentToolTip != null)
this.CurrentToolTip.Opened += Tooltip_Opened;
}
this.CurrentToolTip.PlacementTarget = this.AssociatedObject;
this.CurrentToolTip.IsOpen = true;
}
}
}
private async void Tooltip_Opened(object sender, RoutedEventArgs e)
{
await Task.Delay(1000);
(this.AssociatedObject.ToolTip as ToolTip).IsOpen = false;
}
}
}
Which you would then use like this:
<TextBlock Text="{Binding Path=Text}"
Foreground="{Binding Path=Foreground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListViewItem}}}"
Grid.Column="1"
Margin="5,0,0,0">
<i:Interaction.Behaviors>
<behaviors:ToolTipBehavior ListViewItem="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListViewItem}}}" />
</i:Interaction.Behaviors>
<TextBlock.ToolTip>
<ToolTip Content="{Binding Path=Text}" ToolTipService.ShowDuration="2"
...etc...

WPF: DataTrigger not firing

In a WPF Project I have some restyled DataGridColumnHeaders of a DataGrid which show a ComboBox for each DataGridColumnHeader. When the user selects from the ComboBox's the SelectionChanged handler (in the code behind) updates an Array of ColumnOptionViewModel objects on the MainWindowViewModel with the newest selection.
At this point some code also works out if there are any duplicate selections in this array, and then sets an IsDuplicate Boolean property on the ColumnOptionViewModel that are duplicates. The idea is that a DataTrigger picks up the change in IsDuplicate and changes the Background of a TextBlock in the DataTemplate of the ItemTemplate for the duplicate ComboBox's to Red.
However, this trigger is not firing. The IsDuplicate properties are being set ok, and everything else works as expected. What am I doing wrong?
Here is the XAML for the Window:
<Window x:Class="TestDataGrid.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestDataGrid"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid Grid.Row="1" x:Name="dataGrid" ItemsSource="{Binding Records}">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<ComboBox x:Name="cbo"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},Path=DataContext.ColumnOptions}"
SelectionChanged="cbo_SelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock x:Name="txt" Text="{Binding Name}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=cbo, Path=SelectedItem.IsDuplicate}">
<Setter TargetName="txt" Property="Background" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.ColumnHeaderStyle>
</DataGrid>
</Grid>
CODE BEHIND:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel(RecordProvider.GetRecords());
}
private void cbo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var vm = (MainWindowViewModel)DataContext;
var selectionChangedCombo = (ComboBox)e.Source;
var dataGridColumnHeader = selectionChangedCombo.TemplatedParent as DataGridColumnHeader;
vm.ColumnSelections[dataGridColumnHeader.DisplayIndex] = selectionChangedCombo.SelectedItem as ColumnOptionViewModel;
CheckForDuplicates();
}
private void CheckForDuplicates()
{
var vm = (MainWindowViewModel)DataContext;
var duplicates = vm.ColumnSelections.GroupBy(x => x.Name)
.Where(g => g.Skip(1).Any())
.SelectMany(g => g);
foreach (var option in duplicates)
{
option.IsDuplicate = true;
}
}
}
MainWindowViewModel :
public class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<ColumnOptionViewModel> _columnOptions = new ObservableCollection<ColumnOptionViewModel>();
public ObservableCollection<RecordViewModel> _records = new ObservableCollection<RecordViewModel>();
ColumnOptionViewModel[] _columnSelections = new ColumnOptionViewModel[3];
public MainWindowViewModel(IEnumerable<Record> records)
{
foreach (var rec in records)
{
Records.Add(new RecordViewModel(rec));
}
ColumnOptions.Add(new ColumnOptionViewModel(TestDataGrid.ColumnOptions.ColumnOption1));
ColumnOptions.Add(new ColumnOptionViewModel(TestDataGrid.ColumnOptions.ColumnOption2));
ColumnOptions.Add(new ColumnOptionViewModel(TestDataGrid.ColumnOptions.ColumnOption3));
ColumnSelections[0] = ColumnOptions[0];
ColumnSelections[1] = ColumnOptions[1];
ColumnSelections[2] = ColumnOptions[2];
}
public ObservableCollection<ColumnOptionViewModel> ColumnOptions
{
get { return _columnOptions; }
set { _columnOptions = value; }
}
public ColumnOptionViewModel[] ColumnSelections
{
get { return _columnSelections; }
set { _columnSelections = value; }
}
public ObservableCollection<RecordViewModel> Records
{
get { return _records; }
set { _records = value; }
}
}
ColumnOptionViewModel :
public class ColumnOptionViewModel : ViewModelBase
{
ColumnOptions _colOption;
public ColumnOptionViewModel(ColumnOptions colOption )
{
_colOption = colOption;
}
public string Name
{
get { return _colOption.ToString(); }
}
public override string ToString()
{
return Name;
}
private bool _isDuplicate = false;
public bool IsDuplicate
{
get { return _isDuplicate; }
set
{ _isDuplicate = value;
OnPropertyChanged();
}
}
}
EDIT:
ViewModelBase :
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
If you are trying to bind to the IsDuplicate property of the SelectedItem in the ComboBox you could use a RelativeSource.
You should also set the Value property of the DataTrigger to true/false depending on when you want the Background property of the TextBlock to be set to Red:
<ComboBox x:Name="cbo" ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},Path=DataContext.ColumnOptions}"
SelectionChanged="cbo_SelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock x:Name="txt" Text="{Binding Name}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=SelectedItem.IsDuplicate, RelativeSource={RelativeSource AncestorType=ComboBox}}" Value="True">
<Setter TargetName="txt" Property="Background" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
As #mm8 said, the Value is not chosen in your DataTrigger.
If that doesn't work, you can try using Trigger directly on TextBlock instead of DataTemplate.Triggers:
<TextBlock>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Background" Value="White"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsDuplicate}" Value="True">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
Also, background and foreground values in controls that have selectable items can be tricky. For example, you may want to disable default selection colors (unfortunately then you will have to manage selected/focused background yourself):
<ComboBox.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent" />
</ComboBox.Resources>

CanUserAddRows New Row not saving in DataGrid

Created DataGrid and set CanUserAddRows="True"
Have a button which saves updates in the cs file:
private void Save_Click(object sender, RoutedEventArgs e)
{
UnitService unitService = new UnitService();
unitService.SaveUpdates(valuationCase);
MainWindow mainWin = new MainWindow();
mainWin.Show();
this.Close();
}
There is also a textbox not in the datagrid on the window which is editable and this is correctly saving edits with the save click button. Just the new rows aren't.
Any ideas??
datagrid definition:
<DataGrid Name="dgCommentsList" AutoGenerateColumns="False" Margin="10,196,9.953,38.204" CanUserAddRows="True" FontSize="18">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="FontSize" Value="20" />
<Setter Property="FontWeight" Value="bold" />
</Style>
</DataGrid.ColumnHeaderStyle>
<DataGrid.Columns>
<DataGridTemplateColumn Header="Type" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox x:Name="Type" Text="{Binding Type}" >
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding IsReadOnly}" Value="False">
<Setter Property="TextBox.IsReadOnly" Value="False"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsReadOnly}" Value="True">
<Setter Property="TextBox.IsReadOnly" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid>
I think you need to set the mode of the binding for it to write back to the underlying object.Plus I noticed your DataGrid does not have an ItemsSource. I'm guessing as this was just a snippet that you left it out.
<TextBox x:Name="Type" Text="{Binding Type, Mode=TwoWay}">
You should commit the edit on the row using dataGrid.CommitEdit()
Edit: After diagnosing the issue here goes
You either need to implement INotifyPropertyChanged on your DataContext class (i.e: Viewmodel) like so:
public class ViewModel: INotifyPropertyChanged
{
private string _type;
public string Type
{
get { return _type; }
set
{
_type = value;
OnPropertyChanged("Type");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Or you extend from DependencyObject and use Dependency Properties, like so:
public class ViewModel: DependencyObject
{
public static readonly DependencyProperty TypeProperty = DependencyProperty.Register(
"Type", typeof (string), typeof (ViewModel), new PropertyMetadata(default(string)));
public int Type
{
get { return (int) GetValue(TypeProperty ); }
set { SetValue(TypeProperty , value); }
}
}
Hope it helps ;)

WPF window doesn't get updated by property binding

I have a WPF form with a textbox which is defined in xaml file as follows:
<TextBox Grid.Column="1" Grid.Row="9" TabIndex="0" x:Name="txtboxExample" Width="170" >
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Visibility" Value="Hidden" />
<Style.Triggers>
<DataTrigger Binding="{Binding ToolDataContext.ItemInstance.IsToShow, Mode=TwoWay}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
<TextBox.Text>
<Binding Path="ToolDataContext.ItemInstance.UserText" UpdateSourceTrigger="PropertyChanged" />
</TextBox.Text>
</TextBox>
...
<Button Click="someBtn_Click" Content="{x:Static res:Strings.ButtonString}" Name="someButton">
on the xaml.cs file I have the following code:
private void someBtn_Click(object sender, RoutedEventArgs e)
{
...
ToolDataContext.ItemInstance.IsToShow = true;
...
}
In the Item class I have the following code for the property IsToShow:
public class Item : SyncableObject, ISearchableObject, INotifyPropertyChanged
{
...
private bool _isToShow;
public bool IsToShow
{
get { return _isToShow; }
set
{
if (value == _isToShow)
return;
_isToShow = value;
this.OnPropertyChanged("IsToShow");
}
}
...
new public event PropertyChangedEventHandler PropertyChanged;
new public void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
...
}
I would expect the window to show the textbox when I click when I on the button.
But it doesn't happen.
Can anyone give me a lead about what am I doing wrong?
Try adding Path to your data trigger binding
<DataTrigger Binding="{Binding Path=ToolDataContext.ItemInstance.IsToShow, Mode=TwoWay}" Value="True">
<DataTrigger Binding="{Binding ToolDataContext.ItemInstance.IsToShow, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Value="True">
Assuming that you have assigned the datacontext, you need to add UpdateSourceTrigger=PropertyChanged

Categories

Resources