Ketfera
Ketfera

Reputation: 43

Binding a DependencyObject outside the Logical Tree to the property an element in the Logical Tree

Edited

Problem Summary:

I have a custom control that has an ObservableCollection of DependencyObjects. Because the DependencyObjects are not children of the control, they are not in the Logical Tree. However, I need them to bind to properties of elements in the Logical Tree using XAML. (I can't use code-behind.) I tried using Source={x:Reference blah}, but I am unable to use it because of cyclical dependency restrictions.

Does anyone know how I can add the DependencyObjects to the Logical Tree? Or does anyone have any other ideas how to work around this issue?

Details:

I am developing a custom ComboBox. I want one of my ComboBoxes to filter the items visible according to the values selected in other ComboBoxes on the same window.

Example:

One ComboBox displays a list of products stored in my database, and another displays the product types. I want the second ComboBox to filter the visible items of the first when an item is selected, and I want the first ComboBox to filter the visible items and set the value of the second.

Because of the way I have the "ProductTypes" table set up, the "typeName" field is not unique, so if I want my ComboBox to only show unique names of types of products, then I must use dataTable.DefaultView.ToTable(unique: true, column: "typeName").DefaultView.

Code:

The custom ComboBox has an ObservableCollection of FilterBinding objects, which bind to the selected values of the other ComboBoxes. Here is the FilterBinding class:

public class FilterBinding : DependencyObject
{
    public object Value { get { return GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } }
    public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(FilterBinding), new FrameworkPropertyMetadata(null, ValueChanged));
    public static void ValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        FilterBinding binding = d as FilterBinding;
        binding.isActive = e.NewValue.IsNotNullString();
        binding.parent.FilterItems();
    }

    public bool IsActive { get { return isActive; } }
    bool isActive = false;
    public string Path { get; set; }
    public IonDataComboBox Parent { get; set; }
}

Here is the code for my custom ComboBox. It actually inherits from Telerik's RadComboBox, but it behaves pretty much just like a normal ComboBox.

public class IonDataComboBox : RadComboBox, IPopulatable
{
    public object BindingValue { get { return GetValue(BindingValueProperty); } set { SetValue(BindingValueProperty, value); } }
    public static readonly DependencyProperty BindingValueProperty = DependencyProperty.Register("BindingValue", typeof(object), typeof(IonDataComboBox), new FrameworkPropertyMetadata(null));

    public object SelectedValueBinding { get { return GetValue(SelectedValueBindingProperty); } set { SetValue(SelectedValueBindingProperty, value); } }
    public static readonly DependencyProperty SelectedValueBindingProperty = DependencyProperty.Register("SelectedValueBinding", typeof(object), typeof(IonDataComboBox), new FrameworkPropertyMetadata( null, SelectedValueBindingChanged));
    public static void SelectedValueBindingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as IonDataComboBox).SetSelectedValueFromBinding();
    }

    public List<DbPortal.DelegatedQuery> Queries { get { return queries; } }
    protected List<DbPortal.DelegatedQuery> queries = new List<DbPortal.DelegatedQuery>();
    public string PopulateCommand { get; set; }

    public ObservableCollection<FilterBinding> FilterBindings { get; set; }

    List<int> bindingsFilteredIndices;
    Collection<int> textFilteredIndices = new Collection<int>();

    DataTable dataTable;

    public IonDataComboBox()
        : base()
    {
        QueryParameters = new List<DbParameter>();
        FilterBindings = new ObservableCollection<FilterBinding>();
    }

    public void Populate()
    {
        //archaic
        if (PopulateCommand.IsNotNullString()) {
            queries.Add(PopulateQueryCompleted);
            if (QueryParameters.Count > 0)
                new DbPortal().ExecuteReader(this, queries.Count - 1, PopulateCommand);
        }
    }

    void PopulateQueryCompleted(object result, int queryID)
    {
        dataTable = result as DataTable;

        DataView dataView;
        if (SelectedValuePath.IsNotNullString())
            dataView = dataTable.DefaultView;
        else
            dataView = dataTable.DefaultView.ToTable(true, DisplayMemberPath).DefaultView;

        dataView.Sort = DisplayMemberPath + " asc";
        ItemsSource = dataView;

        FilterItems();
    }

    void SetSelectedValueFromBinding()
    {
        if (SelectedValueBinding.IsNullString())
            return;

        string path = SelectedValuePath.IsNotNullString() ? SelectedValuePath : DisplayMemberPath;

        foreach (DataRowView item in ItemsSource) {
            if (item[path].Equals(SelectedValueBinding)) {
                SelectedItem = item;
                break;
            }
        }
    }

    List<int> FindIndicesOfItems(DataRow[] filteredItems)
    {
        List<int> indices = new List<int>();
        DataView filteredItemsView;

        if (SelectedValuePath.IsNotNullString())
            filteredItemsView = filteredItems.CopyToDataTable().DefaultView;
        else
            filteredItemsView = filteredItems.CopyToDataTable().DefaultView.ToTable(true, DisplayMemberPath).DefaultView;

        filteredItemsView.Sort = DisplayMemberPath + " asc";

        int i = 0;
        foreach (DataRowView item in filteredItemsView) {
            while (i < Items.Count) {
                if (item[DisplayMemberPath].Equals((Items[i] as DataRowView)[DisplayMemberPath])) {
                    indices.Add(i++);
                    break;
                } else
                    i++;
            }
        }

        return indices;
    }

    public void FilterItems()
    {
        if (ItemsSource.IsNull())
            return;

        DataRow[] filteredItems = dataTable.Select();

        foreach (FilterBinding binding in FilterBindings) {
            if (binding.IsActive)
                filteredItems = filteredItems.Where(r => r[binding.Path].Equals(binding.Value)).ToArray();
        }

        if (filteredItems.Length > 0) {
            bindingsFilteredIndices = FindIndicesOfItems(filteredItems);

            UpdateItemsVisibility(false, null);

            if (bindingsFilteredIndices.Count == 1) {
                SelectedIndex = bindingsFilteredIndices[0];

                if (SelectedItem is DataRowView)
                    BindingValue = (SelectedItem as DataRowView)[SelectedValuePath.IsNotNullString() ? SelectedValuePath : DisplayMemberPath];
                else
                    BindingValue = SelectedItem;
            }
        }
    }

    protected override void UpdateItemsVisibility(bool showAll, Collection<int> matchIndexes)
    {
        if (matchIndexes.IsNotNull())
            textFilteredIndices = matchIndexes;

        for (int i = 0; i < Items.Count; i++) {
            FrameworkElement element = ItemContainerGenerator.ContainerFromItem(Items[i]) as FrameworkElement;
            if (element.IsNotNull()) {
                bool isMatch =
                        textFilteredIndices.Count > 0 ? textFilteredIndices.Contains(i) : true &&
                        bindingsFilteredIndices.Contains(i) &&
                        Items[i] is DataRowView ?
                                (Items[i] as DataRowView)[DisplayMemberPath].IsNotNullString() :
                                Items[i].IsNotNullString();

                var visibility = showAll || isMatch ? Visibility.Visible : Visibility.Collapsed;

                element.Visibility = visibility;
            }
        }
    }

    protected override void OnInitialized(EventArgs e)
    {
        base.OnInitialized(e);
        DefaultStyleKey = typeof(IonDataComboBox);

        foreach (FilterBinding binding in FilterBindings)
            binding.Parent = this;

        Populate();
    }

    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        base.OnItemsSourceChanged(oldValue, newValue);

        if (!IsDropDownOpen) {
            IsDropDownOpen = true;
            IsDropDownOpen = false;
        }
    }

    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);

        if (IsFilteringItems || !IsDropDownOpen)
            return;

        if (e.AddedItems[0] is DataRowView)
            BindingValue = (e.AddedItems[0] as DataRowView)[SelectedValuePath.IsNotNullString() ? SelectedValuePath : DisplayMemberPath];
        else
            BindingValue = e.AddedItems[0];
    }
}

Here is the XAML:

<Window x:Class="FluorideDrive.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:iwcd="clr-namespace:IonDrive.Windows.Controls.Data;assembly=IonDrive"
        x:Name="window" Width="300" Height="400">

    <StackPanel>
        <iwcd:IonDataComboBox x:Name="combo"
                              DisplayMemberPath="CompanyName"
                              PopulateCommand="SELECT * FROM Company"
                              SelectedValuePath="Tid"
                              SelectedValueBinding="{Binding Tid}"
                              IsEditable="True"
                              IsFilteringEnabled="True">
            <iwcd:IonDataComboBox.FilterBindings>
                <iwcd:FilterBinding Path="City" Value="{Binding BindingValue, Source={x:Reference combo1}}"/>
            </iwcd:IonDataComboBox.FilterBindings>
        </iwcd:IonDataComboBox>

        <iwcd:IonDataComboBox x:Name="combo1"
                              DisplayMemberPath="City"
                              PopulateCommand="SELECT * FROM Company"
                              SelectedValueBinding="{Binding City}"
                              IsEditable="True"
                              IsFilteringEnabled="True">
            <iwcd:IonDataComboBox.FilterBindings>
                <iwcd:FilterBinding Path="Tid" Value="{Binding BindingValue, Source={x:Reference combo}}"/>
            </iwcd:IonDataComboBox.FilterBindings>
        </iwcd:IonDataComboBox>
    </StackPanel>
</Window>

However, it doesn't bind the FilterBindings because the ElementName only works for elements in the Logical Tree.

I don't use MVVM. Instead I am getting a DataTable through SQL. Eventually I will use EntityFramework, but it won't change the fact that the ItemsSource will be assigned to a DataView derived from LINQ. The reason I need to use DataView is because sometimes the DisplayMemberPath will refer to a column that has non-unique entries, which need to be displayed as unique in the ComboBox.

Upvotes: 1

Views: 805

Answers (1)

Sheridan
Sheridan

Reputation: 69959

Surely your required functionality would be achieved much easier if you do your filtering in your view model or code behind? Just attach selection changed handlers to your ComboBoxes and update the ItemsSource property of each of the other ComboBoxes dependant on the selection.

When I do this kind of thing, I have two collection properties for each of my collection controls:

public ObservableCollection<SomeType> Items
{
    get { return items; }
    set
    {
        if (items != value) 
        {
            items= value; 
            NotifyPropertyChanged("Items");
            FilterItems();
        }
    }
}

public ObservableCollection<SomeType> FilteredItems
{
    get { return filteredItems ?? (filteredItems = Items); }
    private set { filteredItems = value; NotifyPropertyChanged("FilteredItems"); }
}

private void FilterItems()
{
    filteredItems = new ObservableCollection<SomeType>();
    if (filterText == string.Empty) filteredItems.AddRange(Items);
    else filteredItems.Add(AudioTracks.Where(m => CheckFields(m)));
    NotifyPropertyChanged("FilteredItems");
}

private bool CheckFields(SomeType item)
{
    return your.BoolCondition.Here;
}

public string FilterText
{
    get { return filterText; }
    set
    {
        if (filterText != value)
        {
            filterText = value;
            NotifyPropertyChanged("FilterText");
            FilterItems();
        }
    }
}

In this example, I have a FilterText property which triggers the filtering of the collection, but in your example, you would call this FilterItems method from the SelectionChanged handlers instead. In my UI, I bind to the FilteredItems property, not the Items property... this way, I always have all of the possible values stored in Items and the collection controls only show the filtered values.

Please note that I have adapted this code from one of my projects where I have substituted a custom collection type that allows me to add multiple items to it at once for ObservableCollection<T> which does not.

Upvotes: 0

Related Questions