DataBinding Failure using Custom Dependency Properties

487 Views Asked by At

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.

2

There are 2 best solutions below

6
Clemens On BEST ANSWER

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.

9
ndonohoe On

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.