Reputation: 43
Edited
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?
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.
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
.
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
.
Upvotes: 1
Views: 805
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 ComboBox
es and update the ItemsSource
property of each of the other ComboBox
es 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