DataTrigger on each item's textbox in ItemsControl - c#

I have an ItemsControl that displays a list of Labels & TextBoxes that are used for data input and a button that executes some command when pressed (using the input values):
<DataTemplate x:Key="StringParameterTemplate">
<StackPanel Name="StackPanel_Parameter"
Orientation="Horizontal">
<Label Name="ParameterLabel"
Content="{Binding ParameterLabel}"
HorizontalContentAlignment="Right"
Width="200" />
<TextBox Name="ParameterTextBlock"
Text="{Binding ParameterValue, UpdateSourceTrigger=PropertyChanged}"
Width="300"/>
</StackPanel>
</DataTemplate>
. . .
<!-- Display all parameters -->
<ItemsControl Name="ListView_Parameters"
ItemsSource="{Binding ParameterCollection, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}"
ItemTemplateSelector="{StaticResource TaskParameterTemplateSelector}"
BorderThickness="0" />
<Button Name="ExecuteTaskButton"
Content="{Binding ButtonLabel}"
Style="{StaticResource ExecuteButtonStyle}"
Command="ExecuteTask">
I would like to create a style that enables/disables the button when ANY of the parameters from ListView_Parameters is empty. Something like this:
<!-- Execute button enable / disable -->
<Style x:Key="ExecuteButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Button.IsEnabled" Value="True" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=ListView_Parameters, Path=ParameterValue}" Value="">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>

You can achieve this with a single binding using a converter.
<Button Content="{Binding ButtonLabel}"
IsEnabled="{Binding Path=ItemsSource,
RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}},
Converter={local:ItemsToBooleanConverter}}" />
Then your converter takes an input of the itemssource (for example a list of objects) - and can return true if all fields you want have values, false otherwise.
The converter is mostly boilerplate, but would look like something this:
public class ItemsToBooleanConverter : MarkupExtension, IValueConverter
... but the important part would like like this, if you were using a list:
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var items = value as IList<ParameterList>;
return !(items.Any( <you check here for empty values> );
}
You'll need to be sure your parameter entry fields are bound properly to their sources so that the converter check is current.

Related

WPF ComboBox disabled item still selectable on border

When I disable some combobox items they stays selectable on left and right borders of nested textblock.
I've tried to set margins of textbox and padding of combobox items to 0, then I've tried set HorizontalAlignment property of textbox and combobox item to "Stretch", with no result.
WPF:
<Window.Resources>
<local:ComboboxItemsDisableConverter x:Key="ComboboxItemsDisableConverter"/>
</Window.Resources>
<ComboBox x:Name="comboBox" HorizontalAlignment="Right" Margin="0,13,10,0" Width="441"
SelectedIndex="{Binding ViewModel.SelectedNic, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}"
ItemsSource="{Binding ViewModel.NICs, Mode=OneWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}"
SelectionChanged="ComboBox_SelectionChanged"
IsReadOnly="True" Height="25" VerticalAlignment="Top" Grid.Row="2">
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ComboBox.ItemContainerStyle>
<ComboBox.ItemTemplate>
<DataTemplate>
<ComboBoxItem IsEnabled="{Binding OperationalStatus, Converter={StaticResource ComboboxItemsDisableConverter}}" >
<TextBlock HorizontalAlignment="Stretch" Text="{Binding Description}" />
</ComboBoxItem>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
ComboBoxItemsDisableConverter Class:
class ComboboxItemsDisableConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value == null) return value;
var Status = (OperationalStatus)value;
if (Status != OperationalStatus.Up)
return true;
else
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
What can I do to prevent selection of disabled items completely?
Hiding items works with this code:
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=OperationalStatus, Converter={StaticResource ComboboxItemsDisableConverter}}" Value="true">
<Setter Property="Visibility" Value="Collapsed"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>
If I use this markup
<ComboBox x:Name="comboBox" HorizontalAlignment="Right" Margin="0,13,10,0" Width="441" SelectedIndex="{Binding SelectedNic}" ItemsSource="{Binding NICs}" SelectionChanged="ComboBox_SelectionChanged" IsReadOnly="True" Height="25" VerticalAlignment="Top" Grid.Row="1">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock HorizontalAlignment="Stretch" Text="{Binding Description}" IsEnabled="{Binding OperationalStatus, Converter={StaticResource ComboboxItemsDisableConverter}}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
no items disabled
You were trying to disable the item containers content, instead of the item container.
You must understand that ItemsControl contains items in its ItemsSource. Usually this items are data models. Those models are than wrapped into a container. Data models usually are not of type FrameworkElement, they are plain data types. In order to render elements they must be of type FrameworkElement, that's why the models are wrapped into a container e.g. ComboBoxItem. You can layout the content of this container by defining an ItemTemplate.
You don't interact with the data model (the container content), but with the item container. When you only disable the content you still can interact with the container. The item itself has Padding applied. Therefore there is still enough area to allow interaction.
To solve your problem you must disable the container. To do so you have define a trigger or setter in the ItemContainerStyle. Note that the DataContext of this Style is the data model (the items inside the ItemsSource):
<ComboBox>
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="IsEnabled"
Value="{Binding OperationalStatus, Converter={StaticResource ComboboxItemsDisableConverter}}" />
</Style>
</ComboBox.ItemContainerStyle>
<!-- Remove the IsEnabled binding! -->
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock HorizontalAlignment="Stretch"
Text="{Binding Description}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Note that from a user perspective it is recommended to remove disabled items from the source collection using filtering instead. Don't show content that takes away space and doesn't allow interaction. It can be quite confusing, especially if the user doesn't understand the reason why the items are disabled and how he can get them enabled in order to select them.
ViewModel.cs
class ViewModel
{
public ObservableCollection<MyModel> NICs { get; }
public ViewModel()
{
this.NICs = new ObservableCollection<MyModel>();
// Only show items where OperationalStatus == OperationalStatus.Up
CollectionViewSource.GetDefaultView(this.NICs).Filter =
item => (item as MyModel).OperationalStatus == OperationalStatus.Up;
}
}
MainWindow.xaml
<Window>
<Window.DataContext>
<ViewModel />
</Window.DataContext>
<ComboBox ItemsSource="{Binding NICs}" />
</Window>

Updating Values in the Control Template of the ComboBox

I implemented a column in a data grid that containes comboboxes. In order to display a text box in stead of a combobox when a list containes only one value, I used the solution from this post:
How to hide combobox toggle button if there is only one item?
However, when that one value in the list is changed, it is not updated in the text box. I have, of course, implemented INotifyPropertyChanged and it works as long as I have more than one item in the list (in other words, when the combobox is shown) but the value in the TextBlock is never updated.
Edit:
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox Name="CList" ItemsSource="{Binding Values, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding Path=SelectedValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedIndex="0" BorderBrush="Transparent"
Background="Transparent">
<ComboBox.Style>
<Style TargetType="{x:Type ComboBox}" >
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Items.Count, ElementName=CList}" Value="1">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<TextBlock Text="{Binding Items[0], ElementName=CList}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
I can see you are binding to the item itself, instead of any property in it
so perhaps you many need to bind to the respective property of your data item
eg
<TextBlock Text="{Binding Items[0].MyProperty, ElementName=CList}" />
assuming your intended property is MyProperty
Note if there is no underlying property then you'll have to remove the item and add new one again to list in order to update the text block, in this scenario INotifyPropertyChanged will also not work

How to get first ListBoxItem in the group

I have a ListBox sorted and grouped by SortDescription and GroupDescription and now I want to know whether there is a way to know which item is the first one in each group. For example if I have a list of names I want to get the first Item in the list which starts with A, B, C, etc and change its template.
To be more clear I should say I want to change the DataTemplate of the first ListBoxItem in each group. How is it possible?
Edit
This is my sample CollectionView
<CollectionViewSource
x:Key="Myiew"
Source="{Binding Items}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="FirstName" Converter="{StaticResource StringToFirstLetter}" />
</CollectionViewSource.GroupDescriptions>
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="FirstName" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
<Listbox ItemsSource={"StaticResources Myiew"} />
As I said everything works fine. the Items are sorted and if I change the GroupStyle I see that the items are grouped, but I don't want to set a group style. I just want to change the DataTemplate of the first item in each group.
Here is a sample for you
create a converter class
this will just see if the value passed is null or not
class FirstItemDetector : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value == null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
then comes the declaration of converter where l: is the namespace to your converter class
<l:FirstItemDetector x:Key="FirstItemDetactor" />
then comes the listbox
I added a group style (you can style however you like) solution is group independent
added a data template as ItemTemplate of the listbox
added a DataTrigger on PreviousData with converter as FirstItemDetactor declared as above
in the setter of trigger I changed the foreground to red ( you have many choice including changing styles, show hide elements etc.
here is the listbox
<ListBox ItemsSource="{Binding Source={StaticResource Myiew}}">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Border BorderBrush="Black"
BorderThickness=".5"
Padding="4">
<TextBlock Text="{Binding Name}"
HorizontalAlignment="Center" />
</Border>
<ItemsPresenter Grid.Row="1" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListBox.GroupStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" x:Name="text" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData},Converter={StaticResource FirstItemDetactor}}"
Value="True">
<Setter TargetName="text"
Property="Foreground"
Value="Red" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
main role is played by the datatigger and the converter, it will work independently, with groups or even nested groups.

how to apply a style using a converter in wpf c#?

I am developing a system with voice commands that apply to a grid of parameters.
I want to apply a style to the element being edited, so that the user knows where he is vocally ...
MyView.xaml
<telerik:RadNumericUpDown Name={Binding Element[0].ID} Grid.Column="0" Name="Left" MinWidth="15" FontSize="11" Minimum="0" NumberDecimalDigits="0"
BorderThickness="0" Maximum="30"
IsInteger="True" ShowButtons="False" ShowTextBox="True"
HorizontalContentAlignment="Left" Value="{Binding Element[0].Input, Mode=TwoWay, ElementName=InputViewUserControl}" Background="Transparent" Foreground="#FF858585" />
<telerik:RadNumericUpDown Name={Binding Element[1].ID} Grid.Column="0" Name="Left" MinWidth="15" FontSize="11" Minimum="0" NumberDecimalDigits="0"
BorderThickness="0" Maximum="30"
IsInteger="True" ShowButtons="False" ShowTextBox="True"
HorizontalContentAlignment="Left" Value="{Binding Element[1].Input, Mode=TwoWay, ElementName=InputViewUserControl}" Background="Transparent" Foreground="#FF858585" />
<telerik:RadNumericUpDown Name={Binding Element[2].ID} Grid.Column="0" Name="Left" MinWidth="15" FontSize="11" Minimum="0" NumberDecimalDigits="0"
BorderThickness="0" Maximum="30"
IsInteger="True" ShowButtons="False" ShowTextBox="True"
HorizontalContentAlignment="Left" Value="{Binding Element[2].Input, Mode=TwoWay, ElementName=InputViewUserControl}" Background="Transparent" Foreground="#FF858585" />
.....i have 30 elements So...
If the user says: element one, I'd like to apply style to Element[0]
If you have an idea let me know Thanks :)
You just need one Style in a Resources section and then you need to add one bool IsSelected property to your Element class:
public bool IsSelected { get; set; } // Implement INotifyPropertyChanged interface here
<Style TargetType="{x:Type telerik:RadNumericUpDown}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter Property="Background" Value="LightGreen" />
</DataTrigger>
</Style.Triggers>
</Style>
The Style will colour the Background of the object that has an IsSelected property that is set to True. All you need to do now is to set the IsSelected property to True for the current object and set the previous object's IsSelected value to False.
Note that this Style has no x:Key value... that means that it will be implicitly set on all of your controls without you needing to set the Style on each element manually.

XAML binding with rules

In XAML I can set TwoWay binding to the local settings using the following
<TextBox
Name="TextXYZ"
Text="{Binding Source={x:Static local:Settings.Default},
Path=TextXYZ,
Mode=TwoWay}" />
<CheckBox Content=""
Name="checkBox1"
IsChecked="{Binding Source={x:Static local:Settings.Default},
Path=checkBox1,
Mode=TwoWay}" />
<CheckBox Content=""
Name="checkBoxSaveSettings"
IsChecked="{Binding Source={x:Static local:Settings.Default},
Path=checkBoxSaveSettings, Mode=TwoWay}" />
Is it possible to introduce rules to the binding in XAML so that if checkBoxSaveSettings.IsChecked=true then controls will have twoway binding but if checkBoxSaveSettings.IsChecked=false then the binding mode is another option?
You can achieve what you want with DataTrigger like so:
<TextBox>
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Text" Value="{Binding Source={x:Static local:Settings.Default}, Path=TextXYZ, Mode=OneWay}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Source={x:Static local:Settings.Default}, Path=checkBoxSaveSettings, Mode=OneWay}" Value="True">
<Setter Property="Text" Value="{Binding Source={x:Static local:Settings.Default}, Path=TextXYZ, Mode=TwoWay}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
Your method however sounds somewhat confusing for the user as you can change control value but it won't take effect until some other CheckBox it ticked. I would recommend binding IsEnabled to checkBoxSaveSettings.IsChecked like so:
<TextBox
Text="{Binding Source={x:Static local:Settings.Default}, Path=TextXYZ, Mode=TwoWay}"
IsEnabled="{Binding ElementName=checkBoxSaveSettings, Path=IsChecked}"/>
Not directly, but there are options for this. Here's just one. Create a converter on your binding. For the converter parameter, pass in the checkbox checked value.
<TextBox
Name="TextXYZ"
Text="{Binding Source={x:Static local:Settings.Default},
Path=TextXYZ,
Converter={StaticResource foo},
ConverterParameter = {Binding ElementName="checkBoxSaveSettings", Path="IsChecked",
Mode=TwoWay}" />
Then create a converter called "foo" (whatever you want). Inside it, if the parameter is true, you return the value passed in. If the parameter is false, you can return whatever you want, including the Settings.Default.TextXYZ value so nothing changes.
Another possible option is to incorporate a setter on TextXYZ but only apply the passed value to the private _TextXYZ if some other condition is true. That other condition would be bound to the checkbox IsChecked. That's something that should be done in a ViewModel and not an object class, but it would work in either.

Categories