WPF. Changing CheckBox IsChecked through MultiBinding doesn't triger CheckBox Command - wpf

I have Datagrid in my WPF app view where I use check boxes in row headers.
<DataGrid.RowHeaderTemplate>
<DataTemplate>
<Grid >
<CheckBox BorderThickness="0"
Command="{Binding DataContext.AssignPartsToGroupCommand, RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type UserControl}}}"
>
<CheckBox.CommandParameter>
<MultiBinding Converter="{StaticResource PartsGroupAssignConverter}">
<Binding Path="IsChecked" RelativeSource="{RelativeSource Self}" Mode="OneWay"/>
<Binding RelativeSource="{RelativeSource Mode=FindAncestor,
AncestorType={x:Type DataGridRow}}"
Path="DataContext" Mode="OneWay"/>
</MultiBinding>
</CheckBox.CommandParameter>
<CheckBox.IsChecked>
<MultiBinding Converter="{StaticResource PartsGroupAssignedConverter}" Mode="OneWay">
<Binding ElementName="partsGroupGrid" Path="SelectedItem.id"></Binding>
<Binding RelativeSource="{RelativeSource Mode=FindAncestor,
AncestorType={x:Type DataGridRow}}"
Path="DataContext" Mode="OneWay"/>
<Binding Path="IsSelected" Mode="OneWay"
RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type DataGridRow}}"
/>
</MultiBinding>
</CheckBox.IsChecked>
</CheckBox>
</Grid>
</DataTemplate>
As you can see I bind CheckBox property "IsSelected" to multiple values and one of them is DataGrid row selection:
<Binding Path="IsSelected"
Mode="OneWay"
RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type DataGridRow}}"
/>
My problem is - the command linked to CheckBox is not triggered when I check CheckBox by selecting row. But it triggers when I do it manually (with mouse). How can I solve this?

According to the CheckBox source codes, the desired approach is not supported - a command will be called only after clicking on it.
However, you can create a small CheckBox descendant to achieve the behavior you need. This descendant will track changes of the CheckBox.IsChecked property and execute a command.
You can do it like this:
public class MyCheckBox : CheckBox {
static MyCheckBox() {
IsCheckedProperty.OverrideMetadata(typeof(MyCheckBox), new FrameworkPropertyMetadata((o, e) => ((MyCheckBox)o).OnIsCheckedChanged()));
}
readonly Locker toggleLocker = new Locker();
readonly Locker clickLocker = new Locker();
void OnIsCheckedChanged() {
if (clickLocker.IsLocked)
return;
using (toggleLocker.Lock()) {
OnClick();
}
}
protected override void OnToggle() {
if (toggleLocker.IsLocked)
return;
base.OnToggle();
}
protected override void OnClick() {
using (clickLocker.Lock()) {
base.OnClick();
}
}
}
And the Locker class:
class Locker : IDisposable {
int count = 0;
public bool IsLocked { get { return count != 0; } }
public IDisposable Lock() {
count++;
return this;
}
public void Dispose() { count--; }
}

Related

Applying a (non-multi) ValueConverter the output of a MultiBinding + StringFormat

Is there a way to apply a (single, not multi) ValueConverter to the output of a MultiBinding which uses StringFormat (i.e. after the string has been formatted).
It would be the equivalent of that code, in which I used an intermediary collapsed TextBlock to do the trick :
<StackPanel>
<TextBox x:Name="textBox1">TB1</TextBox>
<TextBox x:Name="textBox2">TB2</TextBox>
<TextBlock x:Name="textBlock" Visibility="Collapsed">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1}">
<Binding ElementName="textBox1" Path="Text"/>
<Binding ElementName="textBox2" Path="Text"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock Text="{Binding ElementName=textBlock,
Path=Text, Converter={StaticResource SingleValueConverter}}" />
</StackPanel>
Here is a hack that does what you want:
public static class Proxy
{
public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached(
"Text",
typeof(string),
typeof(Proxy),
new PropertyMetadata(string.Empty));
public static void SetText(this TextBlock element, string value)
{
element.SetValue(TextProperty, value);
}
[AttachedPropertyBrowsableForChildren(IncludeDescendants = false)]
[AttachedPropertyBrowsableForType(typeof(TextBlock))]
public static string GetText(this TextBlock element)
{
return (string) element.GetValue(TextProperty);
}
}
<StackPanel>
<TextBox x:Name="textBox1">TB1</TextBox>
<TextBox x:Name="textBox2">TB2</TextBox>
<TextBlock Text="{Binding Path=(local:Proxy.Text),
RelativeSource={RelativeSource Self},
Converter={StaticResource SingleValueConverter}}">
<local:Proxy.Text>
<MultiBinding StringFormat="{}{0}{1}">
<Binding ElementName="textBox1" Path="Text" />
<Binding ElementName="textBox2" Path="Text" />
</MultiBinding>
</local:Proxy.Text>
</TextBlock>
</StackPanel>
If you look at the MultiBinding.Converter Property page on MSDN, you will see that you can provide a Converter for a MultiBinding. However, it is not a normal IValueConverter, instead it requires an IMultiValueConverter. It can be used like this:
<TextBlock x:Name="textBlock" Visibility="Collapsed">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1}" Converter="{StaticResource Converter}"
ConverterParameter="SomeValue">
<Binding ElementName="textBox1" Path="Text"/>
<Binding ElementName="textBox2" Path="Text"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
An example of an IMultiValueConverter implementation can be found in the linked pages.

Validate two TextBox.Text values to be distinct

Grab two TextBox and say that you need to validate that their content are distinct strings.
Example :
Correct result : prefix1, prefix2
Incorrect result : prefix1, prefix1
To do that task I thought about using a MultiBinding but two problems arises then :
Where should it be placed ? currently it is on a dummy TextBox
Even using that dummy TextBox, the ValidationRule is never called
Not sure whether this approach is correct, how would you do that ?
<MultiBinding Converter="{StaticResource MyConverter}">
<Binding Path="GradientPrefix"
Source="{StaticResource Indices}"
UpdateSourceTrigger="PropertyChanged" />
<Binding Path="ColorPrefix"
Source="{StaticResource Indices}"
UpdateSourceTrigger="PropertyChanged" />
<MultiBinding.ValidationRules>
<gpl2Xaml:DistinctStringValidationRule />
</MultiBinding.ValidationRules>
</MultiBinding>
Here's the solution using a BindingGroup !
Error at BindingGroup level :
Error at BindingGroup and field levels :
No errors :
Here's the code :
<Window>
<Window.Resources>
<gpl2Xaml:Indices x:Key="Indices"
ColorIndex="1"
ColorPrefix="MyColor"
GradientIndex="1"
GradientPrefix="MyColor" />
</Window.Resources>
<Grid DataContext="{StaticResource Indices}"
Style="{StaticResource gridInError}"
Validation.ErrorTemplate="{StaticResource validationTemplate}">
<Grid.BindingGroup>
<BindingGroup>
<BindingGroup.ValidationRules>
<gpl2Xaml:DistinctValidationRule />
</BindingGroup.ValidationRules>
</BindingGroup>
</Grid.BindingGroup>
<TextBox x:Name="TextBoxGradientPrefix"
Style="{StaticResource textBoxInError}"
TextChanged="TextBoxGradientPrefix_OnTextChanged"
Validation.ErrorTemplate="{StaticResource validationTemplate}">
<Binding Path="GradientPrefix"
Source="{StaticResource Indices}"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<gpl2Xaml:StringValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox>
<TextBox x:Name="TextBoxColorPrefix"
Style="{StaticResource textBoxInError}"
TextChanged="TextBoxColorPrefix_OnTextChanged"
Validation.ErrorTemplate="{StaticResource validationTemplate}">
<Binding Path="ColorPrefix"
Source="{StaticResource Indices}"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<gpl2Xaml:StringValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox>
</Grid>
</Window>
Extra code to trigger validation every time :
private void TextBoxGradientPrefix_OnTextChanged(object sender, TextChangedEventArgs e)
{
grid.BindingGroup.CommitEdit();
}
private void TextBoxColorPrefix_OnTextChanged(object sender, TextChangedEventArgs e)
{
grid.BindingGroup.CommitEdit();
}
And the validation rule :
public class DistinctValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
var bindingGroup = value as BindingGroup;
if (bindingGroup == null) return new ValidationResult(false, "Not a BindingGroup");
var o = bindingGroup.Items[0] as Indices;
if (o == null) return new ValidationResult(false, "Not an Indices");
if (o.ColorPrefix == o.GradientPrefix)
return new ValidationResult(false, "Color prefix and Gradient prefix must be distinct.");
return new ValidationResult(true, null);
}
}

How to clear a TextBox in MVVM?

I have a TextBox in a DataTemplate declared as follows:
<TextBox Grid.Row="1" Grid.Column="1" Margin="0,4,0,0">
<i:Interaction.Triggers>
<i:EventTrigger EventName="LostFocus">
<cmd:EventToCommand Command="{Binding DataContext.NotesEnteredCommand,
RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}">
<cmd:EventToCommand.CommandParameter>
<MultiBinding Converter="{StaticResource SimpleMultiValueConverter}">
<Binding Path="Row.OID" />
<Binding Path="Text" RelativeSource="{RelativeSource FindAncestor, AncestorType=TextBox}" />
</MultiBinding>
</cmd:EventToCommand.CommandParameter>
</cmd:EventToCommand>
</i:EventTrigger>
</i:Interaction.Triggers>
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding DataContext.NotesEnteredCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}">
<KeyBinding.CommandParameter>
<MultiBinding Converter="{StaticResource SimpleMultiValueConverter}">
<Binding Path="Row.OID" />
<Binding Path="Text" RelativeSource="{RelativeSource FindAncestor, AncestorType=TextBox}" />
</MultiBinding>
</KeyBinding.CommandParameter>
</KeyBinding>
</TextBox.InputBindings>
What this TextBox basically does is execute a MVVM-Light RelayCommand when the Enter key is pressed or when losing focus.
My problem is that I cannot figure out a way in MVVM to clear the TextBox's Text value through XAML in the above two scenarios. It's very easy with in code-behind, but I can't figure it out in MVVM.
Any ideas?
If the text is part of your data layer and application logic, a string should exist in your Model or ViewModel and be cleared from there
For example,
<TextBox Text="{Binding NewNote}" ... />
and
void NotesEntered(int oid)
{
SaveNewNote(oid);
NewNote = string.Empty;
}
If it's part of the UI layer only, it should just be cleared with code-behind. It's perfectly acceptable to have UI-specific logic in the code-behind the UI, as that still maintains the separation of layers.
NewNoteTextBox_LostFocus(object sender, EventArgs e)
{
(sender as TextBox).Text = string.Empty;
}
NewNoteTextBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Keys.Enter)
(sender as TextBox).Text = string.Empty;
}
You can use a UpdateSourceHelper. This really helped me out calling an event with no code-behind.
See here http://www.wiredprairie.us/blog/index.php/archives/1701
All you have to do is create a class "UpdateSourceHelper", connect it with your xaml like this
xmlns:local="using:WiredPrairie.Converter
and bind it to your TextBox (or whatever you want to bind to)...
<TextBox Height="Auto" Margin="0,6" Grid.Row="1" TextWrapping="Wrap" TabIndex="0"
Text="{Binding Value}"
local:UpdateSourceHelper.IsEnabled="True"
local:UpdateSourceHelper.UpdateSourceText="{Binding Value, Mode=TwoWay}"/>
If you want call LostFocus Event in the Helper, you simply have to add these 2 lines in your Helper:
tb.LostFocus += AttachedTextBoxLostFocus;
tb.LostFocus -= AttachedTextBoxLostFocus;
So it would look like this:
TextBox tb = (TextBox)obj;
if ((bool)args.NewValue)
{
tb.LostFocus += AttachedTextBoxLostFocus;
}
else
{
tb.LostFocus -= AttachedTextBoxLostFocus;
}
Right click on AttachedTextBoxLostFocus and generate the method. Now you can handle the Event like a code-behind event.

Using Multi Binding in telerik Datagrid

How do I pass multiple parameters in command parameters.
here is what I am trying to do:
I want to send Is checked or not (I can do this by introducing a Boolean field to the object bound. But I dont want to do that) and I want to send the selected data object for that row.
<telerik:GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type telerik:GridViewDataControl}}, Path=DataContext.LineItemSelection}">
<CheckBox.CommandParameter>
<MultiBinding Converter="{StaticResource CommandConverter}">
<Binding Path="IsChecked" />
<Binding RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</CheckBox.CommandParameter>
</CheckBox>
</DataTemplate>
</telerik:GridViewColumn.CellTemplate>
UPDATE:
I added a class called selection Item. To see what the converter is getting.
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
SelectionItem si = new SelectionItem();
foreach (var value in values)
{
Type t = value.GetType();
if (t.FullName == "System.Boolean")
si.IsSelected = (bool) value;
else
{
si.SelectedCustomer = value as Customer;
}
}
return si;
}
The type of the second parameter is the checkbox itself if I use
<Binding RelativeSource="{RelativeSource Self}"/>
Here I want the data item that is bound to that row(in this case Customer).
I even tried using
<Binding RelativeSource= "{RelativeSource FindAncestor, AncestorType={x:Type telerik:GridViewColumn}}" Path="DataContext" />
But this is also coming in as null. why is this?
Try out simple binding by passing in converter current binding source, then cast to underlying object and based on IsChecked return a value.
<DataTemplate>
<CheckBox Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type telerik:GridViewDataControl}}, Path=DataContext.LineItemSelection}">
<CheckBox.CommandParameter>
<MultiBinding Converter="{StaticResource CommandConverter}">
<Binding Path="IsChecked" />
<Binding RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</CheckBox.CommandParameter>
</CheckBox>
</DataTemplate>
// CommandConverter should simply return the values
public object Convert(...)
{
return values;
}
Now in command handler you would be able access parameters:
public void OnLineItemSelection(object parameter)
{
object[] parameters = (object[])parameter;
bool isChecked = (double)parameters[0];
var instance = (TYPENAME)parameters[1];
}
using
<CheckBox Name="chkSelection" Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type telerik:GridViewDataControl}}, Path=DataContext.LineItemSelection}">
<CheckBox.CommandParameter>
<MultiBinding Converter="{StaticResource CommandConverter}">
<Binding Path="IsChecked" ElementName="chkSelection" />
<Binding />
</MultiBinding>
</CheckBox.CommandParameter>
</CheckBox>
did the trick

Can I do Text search with multibinding

I have below combo box in mvvm-wpf application. I need to implement "Text search" in this..(along with multibinding). Can anybody help me please.
<StackPanel Orientation="Horizontal">
<TextBlock Text="Bid Service Cat ID"
Margin="2"></TextBlock>
<ComboBox Width="200"
Height="20"
SelectedValuePath="BidServiceCategoryId"
SelectedValue="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}},
Path=DataContext.SelectedBidServiceCategoryId.Value}"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}},
Path=DataContext.BenefitCategoryList}"
Margin="12,0">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock DataContext="{Binding}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}: {1}">
<Binding Path="BidServiceCategoryId" />
<Binding Path="BidServiceCategoryName" />
</MultiBinding>
</TextBlock.Text></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
Unfortunately, TextSearch.Text doesn't work in a DataTemplate. Otherwise you could have done something like this
<ComboBox ...>
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="TextSearch.Text">
<Setter.Value>
<MultiBinding StringFormat="{}{0}: {1}">
<Binding Path="BidServiceCategoryId"/>
<Binding Path="BidServiceCategoryName"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
However this won't work, so I see two solutions to your problem.
First way
You set IsTextSearchEnabled to True for the ComboBox, override ToString in your source class and change the MultiBinding in the TextBlock to a Binding
Xaml
<ComboBox ...
IsTextSearchEnabled="True">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
Source class
public class TheNameOfYourSourceClass
{
public override string ToString()
{
return String.Format("{0}: {1}", BidServiceCategoryId, BidServiceCategoryName);
}
//...
}
Second Way
If you don't want to override ToString I think you'll have to introduce a new Property in your source class where you combine BidServiceCategoryId and BidServiceCategoryName for the TextSearch.TextPath. In this example I call it BidServiceCategory. For this to work, you'll have to call OnPropertyChanged("BidServiceCategory"); when BidServiceCategoryId or BidServiceCategoryName changes as well. If they are normal CLR properties, you can do this in set, and if they are dependency properties you'll have to use the property changed callback
Xaml
<ComboBox ...
TextSearch.TextPath="BidServiceCategory"
IsTextSearchEnabled="True">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock DataContext="{Binding}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}: {1}">
<Binding Path="BidServiceCategoryId" />
<Binding Path="BidServiceCategoryName" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
Source class
public class TheNameOfYourSourceClass
{
public string BidServiceCategory
{
get
{
return String.Format("{0}: {1}", BidServiceCategoryId, BidServiceCategoryName);
}
}
private string m_bidServiceCategoryId;
public string BidServiceCategoryId
{
get
{
return m_bidServiceCategoryId;
}
set
{
m_bidServiceCategoryId = value;
OnPropertyChanged("BidServiceCategoryId");
OnPropertyChanged("BidServiceCategory");
}
}
private string m_bidServiceCategoryName;
public string BidServiceCategoryName
{
get
{
return m_bidServiceCategoryName;
}
set
{
m_bidServiceCategoryName = value;
OnPropertyChanged("BidServiceCategoryName");
OnPropertyChanged("BidServiceCategory");
}
}
}
I don't know if your text search has to search ALL the text, but if you want to search from the category ID, you can just set the TextSearch.TextPath property to BidServiceCategoryId. That should also be helpful for anyone who wants to use multibinding and finds that the text search no longer works... It does work if you explicitly set the TextPath property.

Resources