0

I want to highlight all cells in a DataGrid based on the search text in some TextBox in my view. To do this I have the following static class with the required Dependency Properties (DP)

public static class DataGridTextSearch
{
    public static readonly DependencyProperty SearchValueProperty = 
        DependencyProperty.RegisterAttached("SearchValue", typeof(string), typeof(DataGridTextSearch),
              new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.Inherits));

    public static string GetSearchValue(DependencyObject obj) {
        return (string)obj.GetValue(SearchValueProperty);
    }

    public static void SetSearchValue(DependencyObject obj, string value) {
        obj.SetValue(SearchValueProperty, value);
    }

    public static readonly DependencyProperty HasTextMatchProperty =
         DependencyProperty.RegisterAttached("HasTextMatch", typeof(bool), 
            typeof(DataGridTextSearch), new UIPropertyMetadata(false));

    public static bool GetHasTextMatch(DependencyObject obj) {
        return (bool)obj.GetValue(HasTextMatchProperty);
    }

    public static void SetHasTextMatch(DependencyObject obj, bool value) {
        obj.SetValue(HasTextMatchProperty, value);
    }
}

Then in my XAML I have the following

<UserControl x:Class="GambitFramework.TaurusViewer.Views.TaurusViewerView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:Caliburn="http://www.caliburnproject.org"
             xmlns:Converters="clr-namespace:GambitFramework.TaurusViewer.Converters"
             xmlns:Helpers="clr-namespace:GambitFramework.TaurusViewer.Helpers">
    <UserControl.Resources>
        <ResourceDictionary>    
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="../Resources/Styles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
            <Converters:ULongToDateTimeStringConverter x:Key="ULongToDateTimeStringConverter"/>
        </ResourceDictionary>
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBox Name="SearchTextBox"
                 Grid.Row="0"
                 Margin="5"
                 Width="250"
                 VerticalContentAlignment="Center"
                 Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        <TabControl Grid.Row="1">
            <TabItem Header="Events">
                <DataGrid x:Name="EventsGrid"
                          HorizontalAlignment="Stretch"
                          VerticalAlignment="Stretch"
                          AutoGenerateColumns="False" 
                          CanUserAddRows="False" 
                          CanUserDeleteRows="False"
                          SelectionUnit="FullRow"
                          EnableRowVirtualization="True" 
                          EnableColumnVirtualization="True" 
                          IsSynchronizedWithCurrentItem="True"
                          VirtualizingStackPanel.VirtualizationMode="Standard"
                          Helpers:DataGridTextSearch.HasTextMatch="False" 
                          Helpers:DataGridTextSearch.SearchValue="{Binding ElementName=SearchTextBox, Path=Text, UpdateSourceTrigger=PropertyChanged}"
                          GridLinesVisibility="{Binding GridLinesVisibility}" 
                          SelectedItem="{Binding SelectedEvent, Mode=TwoWay}"
                          ItemsSource="{Binding EventsCollection}">
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="Event ID" IsReadOnly="True" Binding="{Binding event_id}"/>
                        <DataGridTextColumn Header="Competition" IsReadOnly="True" Binding="{Binding comp}"/>
                        <DataGridTextColumn Header="Home Team" IsReadOnly="True" Binding="{Binding home}"/>
                        <DataGridTextColumn Header="Away Team" IsReadOnly="True" Binding="{Binding away}"/>
                        <DataGridTextColumn Header="Start Time" 
                                            IsReadOnly="True"
                                            Binding="{Binding start_time, Converter={StaticResource ULongToDateTimeStringConverter}}"/>
                    </DataGrid.Columns>
                    <DataGrid.Resources>
                        <Converters:SearchValueConverter x:Key="SearchValueConverter"/>
                        <Style TargetType="{x:Type DataGridCell}">
                            <Setter Property="Helpers:DataGridTextSearch.HasTextMatch">
                                <Setter.Value>
                                    <MultiBinding Converter="{StaticResource SearchValueConverter}">
                                        <Binding RelativeSource="{RelativeSource Self}" Path="Content.Text"/>
                                        <Binding RelativeSource="{RelativeSource Self}" Path="Helpers:DataGridTextSearch.SearchValue"/>
                                    </MultiBinding>
                                </Setter.Value>
                            </Setter>
                            <Style.Triggers>
                                <Trigger Property="Helpers:DataGridTextSearch.HasTextMatch" Value="True">
                                    <Setter Property="Background" Value="Orange"/>
                                    <!--<Setter Property="Background" Value="{DynamicResource HighlightBrush}"/>-->
                                </Trigger>
                            </Style.Triggers>
                        </Style>
                    </DataGrid.Resources>
                </DataGrid>
            </TabItem>
        </TabControl>
    </Grid>
</UserControl>

Where my ULongToDateTimeStringConverter is defined in Style.xaml and works and the SearchValueConverter is defined as

public class SearchValueConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, 
        object parameter, System.Globalization.CultureInfo culture) {
        string cellText = values[0] == null ? string.Empty : values[0].ToString();
        string searchText = values[1] as string;
        if (!string.IsNullOrEmpty(searchText) && !string.IsNullOrEmpty(cellText))
            return cellText.ToLower().StartsWith(searchText.ToLower());
        return false;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, 
        object parameter, System.Globalization.CultureInfo culture) {
        return null;
    }
}

The problem is that the SearchValueConverter converter only seems to get invoked when the grid is loaded. I have used Snoop to check the bindings and all are green and good. Helpers:DataGridTextSearch.SearchValue is changing in the element inspected with Snoop upon key press, but the converter code is never used/invoked. I believe that this is a DataContext problem but I am not sure how to find out exactly or indeed how to resolve this. My DataContext is being set by Caliburn in the usual way.

I have noticed in Snoop that I am getting

An unhanded exception has occurred on the user interface thread.

Message: Cannot set Expression. It is marked as 'NonShareable' and has already been used. Stacktrace: at System.Windows.DependencyObject.SetValueCommon(DependencyProperty dp, Object value, PropertyMetadata metadata, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType, Boolean isInternal) at System.Windows.DependencyObject.SetValue(DependencyProperty dp, Object value) ...

When I "Delve Binding Expression" on DataGridTextSearch.SearchValue. This might be a Snoop problem, but I though it might be related to the issue I am having.

What am I doing wrong here?

Thanks for your time.

MoonKnight
  • 23,214
  • 40
  • 145
  • 277

2 Answers2

3

Without having tested your code, the binding path should be

Path="(Helpers:DataGridTextSearch.SearchValue)"

because it is an attached property. See PropertyPath XAML Syntax on MSDN.

Clemens
  • 123,504
  • 12
  • 155
  • 268
  • 1
    I haven't tested this either, but I think it may be correct. I recall reading somewhere that paraenthesis are needed when binding to an attached property – Rachel Feb 12 '15 at 14:55
  • @Rachel They are required in property paths in bindings, as pointed out in the linked article. – Clemens Feb 12 '15 at 14:56
  • There's also still the other problem of binding to `DataGridCell` instead of `DataGrid` as [bonyjoe pointed out](http://stackoverflow.com/a/28476750/302677) though, and I'm not entirely positive but shouldn't the DP definition be `typeof(DataGrid)` instead of `typeof(DataGridTextSearch)` to specify it's an attached property for a `DataGrid`? – Rachel Feb 12 '15 at 15:01
  • 1
    @Rachel No, the so-called "owner type" is always the class that declares the dependency property. Think about `Canvas.Top`, which is declared in Canvas, but can be applied to all sorts of elements. And for the correct source object (DataGrid or DataGridCell), that shouldn't matter, as the property value is inherited. – Clemens Feb 12 '15 at 15:02
  • 1
    Ah ok, thanks :) It's been a while since I've gotten to work with WPF, and I didn't notice the `Inherited` flag on the DP. – Rachel Feb 12 '15 at 15:11
  • That was it. God dam', I love WPF, but things like this drive me crazy! – MoonKnight Feb 12 '15 at 15:12
1

From what I can see it looks like your problem is this binding

<Binding RelativeSource="{RelativeSource Self}" Path="Helpers:DataGridTextSearch.SearchValue"/>

This is binding to the AttachedProperty on DataGridCell, rather than what you want to bind to which is the attached property on the DataGrid

This binding should correctly bind to your property

<Binding RelativeSource="{RelativeSource AncestorType={x:Type DataGrid}}" Path="Helpers:DataGridTextSearch.SearchValue"/>

Also you can limit the objects that the attached property can be set on, currently every dependency object will accept your attached property even though you intend it to be DataGrid only.

public static string GetSearchValue(DataGrid obj) {
    return (string)obj.GetValue(SearchValueProperty);
}

public static void SetSearchValue(DataGrid obj, string value) {
    obj.SetValue(SearchValueProperty, value);
}

Changing your property definitions like this will limit the setting and getting of the property to only DataGrid, which should cause your current binding to break and report an error rather than just accepting it but not doing what you expect.

ndonohoe
  • 9,320
  • 2
  • 18
  • 25
  • 1
    "which should cause your current binding to break and report an error". This is not true, since the WPF dependency property system bypasses the CLR wrapper methods when a property is set by a binding (and other sources, too). See [XAML Loading and Dependency Properties](https://msdn.microsoft.com/en-us/library/bb613563.aspx) on MSDN. – Clemens Feb 12 '15 at 12:11
  • Thanks for your reply, unfortunately, although I was with you on the XAML change, this has not worked. I am still confused as to why this is not highlighting the cells. Any further thoughts on this would be appreciated. – MoonKnight Feb 12 '15 at 12:44
  • @Clemens do you have any thoughts on the failing of the above approach? Thanks for your time... – MoonKnight Feb 12 '15 at 12:44
  • What happens if you change the binding path to "DataContext.SearchText"? – ndonohoe Feb 12 '15 at 14:11
  • Also I have to disagree with you Clemens, if he were to change the Get/Set to take a DataGrid object then try to bind to the property Helpers:DataGridTextSearch.SearchValue on a DataGridCell source he would get a binding error saying it doesn't exist. It also stops you from being able to set the property against a non datagrid object. (I have just tested this in a sample project for my sanity) – ndonohoe Feb 12 '15 at 14:19
  • 1
    @bonyjoe That's not a binding error, but just the designer telling you that the property doesn't exist. The binding would still work. – Clemens Feb 12 '15 at 14:40
  • @bonyjoe where exactly do you mean? – MoonKnight Feb 12 '15 at 14:47
  • 1
    And by the way the `RelativeSource Self` binding should also work, because the value of the `DataGridTextSearch.SearchValue` attached property is inherited (`FrameworkPropertyMetadataOptions.Inherits` is set on registration). – Clemens Feb 12 '15 at 14:49
  • @Clemens I am at a loss here, I have checked through this a lot and can't seem to be able to see what I am doing wrong. Any ideas at this point are more than welcome. Thanks again for your time guys... – MoonKnight Feb 12 '15 at 14:52