WPF: DataTrigger not firing - c#

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>

Related

C# Wpf Binding on Textbox in Datagrid doesn't update

for my work I need to generate datagrids via code behind. The datagrids contain 2 columns: Questions (Just text, which should not be editable) and "Answers" which needs a textBox or comboBox based on an enum.
I tried to bind a list of questions (Containing the text and an answer field) to the datagrid. The question column works just fine. But the textboxes don't receive any value. I can type in them, but once I sort the datagrid all values are gone. In the itemsource of the data grid I can see that the values doesn't update at all :/
After this failure I tried this with normal datagrid in XAML but it doesn't work either. The column for answers is a DataGridTemplate Column.
Here is my xaml:
<Window x:Class="BindingTest.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:BindingTest"
xmlns:t="clr-namespace:BindingTest;assembly=BindingTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<DataTemplate x:Key="AnswerTemp">
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Typ}" Value="{x:Static t:QuestionType.Numeric}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBox Text="{Binding Path=Answer,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<DataGrid x:Name="MetaDataGrid" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Questions" Binding="{Binding Text}">
</DataGridTextColumn>
<DataGridTemplateColumn Header="Answers" CellTemplate="{StaticResource ResourceKey=AnswerTemp}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Code behind:
// Just contains a Dictionary <string,QuestionBlock> called SeperatorList
public static ActualPage actualPage = new ActualPage();
public MainWindow()
{
InitializeComponent();
QuestionBlock seperator = new QuestionBlock();
seperator.Questions = new ObservableCollection<Question>();
for (int counter = 1; counter < 11; counter++)
seperator.Questions.Add(new Question() { Text = $"What is the {counter}. Answer?", Answer = $"{5 + counter}", Typ = QuestionType.Numeric, Names = new List<string>() { "1", "2", "3" } });
actualPage.SeperatorList.Add("Test", seperator);
MetaDataGrid.ItemsSource = seperator.Questions;
}
Code in QuestionBlock:
public class QuestionBlock : INotifyPropertyChanged
{
private ObservableCollection<Question> _questionBlock;
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Question> Questions
{
get => _questionBlock;
set
{
_questionBlock = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Questions)));
}
}
}
Code in Question:
public class Question : INotifyPropertyChanged
{
private string _text;
private string _answer;
private QuestionType _typ;
public event PropertyChangedEventHandler PropertyChanged;
public string Text
{
get => _text;
set
{
_text = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text)));
}
}
public string Answer
{
get => _answer;
set
{
_answer = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Answer)));
}
}
public QuestionType Typ
{
get => _typ;
set
{
_typ = value;
}
}
}
What am I doing wrong here? Note: The values that should be included in the comboBox are not enum values! There are values from a string list. Any help would be appreciated :)
You should put the editable control in the CellEditingTemplate of the column:
<DataGridTemplateColumn Header="Answers" CellEditingTemplate="{StaticResource AnswerTemp}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Answer}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
...and bind the TextBox to the Answer property of the DataContext of the ContentControl:
<DataTemplate x:Key="AnswerTemp">
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Typ}" Value="{x:Static t:QuestionType.Numeric}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBox Text="{Binding Path=DataContext.Answer,
RelativeSource={RelativeSource AncestorType=ContentControl}}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>

WPF How correct modify view from ICommand with MVVM

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));
}

Binding a dynamic list to a column of ComboBoxes in a DataGrid only works until actually showing the list

I'm working on a WPF application according to the MVVM pattern and am facing a challenge that I abstracted in the code below.
The app contains a DataGrid with 2 ComboBox columns (each generated in a different manner). The aim is to have a ComboBox present only those items that have not yet been selected by the other ComboBoxes in the same column.
The comboboxes are Bound to an ObservableCollection of Professions. Each profession has a Boolean "Selectable", and a ComboBox should only show those entries with a value of "true".
The list contains:
Painter
Poet
Scientist
To simulate an interactive Command from XAML to the ViewModel, I placed a button that will set the Scientist to Selectable to "false".
App.xaml:
<Application x:Class="wpf_ComboBoxColumn.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
</Application>
MainWindow.xaml.cs:
using System.Windows;
namespace wpf_ComboBoxColumn
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
}
MainWindow.xaml:
<Window x:Class="wpf_ComboBoxColumn.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:wpf_ComboBoxColumn"
xmlns:viewModel="clr-namespace:wpf_ComboBoxColumn"
Title="Combobox Column Binding" Height="350" Width="460">
<Window.Resources>
<ResourceDictionary>
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource {x:Type ComboBox}}">
<Setter Property="ItemsSource"
Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ComboBoxItem" BasedOn="{StaticResource {x:Type ComboBoxItem}}">
<Style.Triggers>
<DataTrigger Binding="{Binding Selectable}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Selectable}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.DataContext>
<viewModel:MainViewModel />
</Grid.DataContext>
<DataGrid ItemsSource="{Binding People}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" />
<DataGridComboBoxColumn
Header="ComboBoxColumn"
SelectedValueBinding="{Binding Description, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
SelectedValuePath="Description"
DisplayMemberPath="Description"
>
<DataGridComboBoxColumn.ElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Professions}"/>
</Style>
</DataGridComboBoxColumn.ElementStyle>
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Professions}"/>
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
<DataGridTemplateColumn Header="TemplateColumn">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
ItemsSource="{Binding Path=DataContext.Professions, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}"
DisplayMemberPath="Description"
SelectedValue="{Binding Profession, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button x:Name="button" Content="Button" HorizontalAlignment="Left" Margin="210,290,0,0" VerticalAlignment="Top" Width="75" Command="{Binding DebugCommand}"/>
</Grid>
</Window>
CustomCommand.cs (ICommand implementation):
using System;
using System.Windows.Input;
namespace wpf_ComboBoxColumn
{
public class CustomCommand: ICommand
{
private readonly Action<object> execute;
public CustomCommand(Action<object> execute)
{
this.execute = execute;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
execute(parameter);
}
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
}
MainViewModel.cs:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace wpf_ComboBoxColumn
{
public class NotifyUIBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Profession
{
public string Description { get; set; }
public Boolean Selectable { get; set; }
}
public class Person
{
public string Name { get; set; }
public string Profession { get; set; }
}
public class MainViewModel : NotifyUIBase
{
public ObservableCollection<Person> People { get; set; }
public ObservableCollection<Profession> Professions { get; set; }
public ICommand DebugCommand { get; set; }
public MainViewModel()
{
DebugCommand = new CustomCommand(Debug);
People = new ObservableCollection<Person>
{
new Person{Name="Tom", Profession="" },
new Person{Name= "Dick", Profession="" },
new Person{Name= "Harry", Profession="" }
};
Professions = new ObservableCollection<Profession>
{
new Profession{ Description="Painter", Selectable=true},
new Profession{ Description="Poet", Selectable=true},
new Profession{ Description="Scientist", Selectable=true},
};
}
private void Debug(object obj)
{
Professions[2].Selectable = false;
}
}
}
Now consider the following scenario (I'm still trying to figure out how to include screen shots):
Open the app: This will show a grid with 3 columns:
First column shows the names "Tom", "Dick" and "Harry".
Second column contains a ComboBox for each person. It requires multiple clicks to open.
Third column also contains a ComboBox for each person. This one is recognizable as such.
Choose "Scientist" for Tom
Click the button (to fake that we executed code that changed Profession.Selectable)
Click on the Combobox for Dick
This will indeed show the remaining Professions (without Scientist), for the rightmost column of ComboBoxes. The leftmost column will still show all options, so this one fails right away.
Click on the Combobox for Tom again
This will, even for the rightmost column of ComboBoxes, show all options again (or rather: still)!
It turns out that the list, once shown, is not dynamically updated. Until we click on it, it is (makes me think of Quantum Mechanics, but that's another story)
The question is: Is there a way to force a refresh of the ItemsSource? Preferrably, of course, respecting MVVM, but at this point, I'll go for any working solution, using either ComboBox-type.
Thanks!
You need to raise the PropertyChanged event on the Selectable property. You're binding to it, and then you're changing it, so if you want the view to change based on this property, it needs to raise PropertyChanged.

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 ;)

Bind Button IsEnabled property to DataTemplate items state and one additional condition

I have an object with editable parameters collection which are bound as a ItemsSource to ItemsControl, and a property which checks if all parameter values are ok. This property bound to button's IsEnabled.
I also want to disable the button when any of textbox has validation error (Validation.HasError == true).
Thanks in advance.
XAML:
<Window x:Class="MyWPFTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<ItemsControl ItemsSource="{Binding Path=MyObject.Parameters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Name}"></TextBlock>
<TextBox Text="{Binding Path=Value, UpdateSourceTrigger=PropertyChanged}"></TextBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button IsEnabled="{Binding Path=MyObject.IsParametersOkay}">OK</Button>
</StackPanel>
</Window>
Code:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace MyWPFTest
{
public partial class MainWindow : Window
{
ObjectWithParameters _MyObject = new ObjectWithParameters();
public ObjectWithParameters MyObject { get { return _MyObject; } }
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
}
public class ObjectWithParameters : INotifyPropertyChanged
{
ObservableCollection<Parameter> _Parameters = new ObservableCollection<Parameter>();
public ObservableCollection<Parameter> Parameters { get { return _Parameters; } }
public event PropertyChangedEventHandler PropertyChanged;
public ObjectWithParameters()
{
var p1 = new Parameter("Parameter 1", 0); p1.PropertyChanged += ParameterChanged; Parameters.Add(p1);
var p2 = new Parameter("Parameter 2", 0); p2.PropertyChanged += ParameterChanged; Parameters.Add(p2);
}
void ParameterChanged(object sender, PropertyChangedEventArgs e)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsParametersOkay"));
}
public bool IsParametersOkay
{
get { return Parameters.FirstOrDefault(p => p.Value < 0) == null; }
}
}
public class Parameter : INotifyPropertyChanged
{
double val;
public double Value
{
get { return val; }
set { val = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Value")); }
}
public string Name { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public Parameter(string name, double value) { Name = name; Value = value; }
}
}
Check out MultiTriggers.
<Style.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Background" Value="#EEEEEE" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasItems" Value="false" />
<Condition Property="Width" Value="Auto" />
</MultiTrigger.Conditions>
<Setter Property="MinWidth" Value="120"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasItems" Value="false" />
<Condition Property="Height" Value="Auto" />
</MultiTrigger.Conditions>
<Setter Property="MinHeight" Value="95"/>
</MultiTrigger>
</Style.Triggers>
This is the way I solved the problem. May be it's not a very elegant solution, but it works.
I added a new property IsFormOkay to MainWindow class, which checks both controls and parameters validity. Then I bound Button.IsEnabled to this property and added TextChanged event for TextBox to notify about IsFormOkay.
Here is code added to MainWindow:
public event PropertyChangedEventHandler PropertyChanged;
public bool IsFormOkay { get { return IsValid(Items) && MyObject.IsParametersOkay; } }
public bool IsValid(DependencyObject obj)
{
if (Validation.GetHasError(obj)) return false;
for (int i = 0, n = VisualTreeHelper.GetChildrenCount(obj); i < n; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (!IsValid(child)) return false;
}
return true;
}
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsFormOkay"));
}
And changes to XAML:
<StackPanel>
<ItemsControl x:Name="Items" ItemsSource="{Binding Path=MyObject.Parameters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Name}" />
<TextBox TextChanged="TextBox_TextChanged" Text="{Binding Path=Value, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button IsEnabled="{Binding Path=IsFormOkay}" Content="OK" />
</StackPanel>

Categories

Resources