Edited
Problem Summary:
I have a custom control that has an ObservableCollection
of DependencyObject
s. Because the DependencyObject
s 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 DependencyObject
s 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 ComboBox
es to filter the items visible according to the values selected in other ComboBox
es 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 ComboBox
es. 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
.