Why does this FilterEventHandler seem to remain in the CollectionViewSource filter even after the filter is set to null? - wpf

Summary:
I'm creating a simple application for practice, where the user can maintain and query a collection of Things. On the UI are several TextBoxes and ComboBoxes with which they can filter the collection.
The three buttons I'm concerned with are [Filter], [Random], and [All]. [Filter] applies the current filter options. [Random] applies the current filter options (if any), and then only shows one random entry from the filtered results. [All], as expected, shows the unfiltered collection.
To fully understand the background for the question, I'll provide the relevant code.
Here is where anything having to do with the CollectionViewSource (or any relevant code I'm posting) gets declared:
//Members
private ObservableCollection<Thing> _myDataCollection;
private CollectionViewSource _CVS;
private Thing _randomThing;
//Properties
public ObservableCollection<Thing> MyDataCollection
{
get { return _myDataCollection; }
set
{
if (_myDataCollection!= value)
{
_myDataCollection= value;
RaisePropertyChanged(() => MyDataCollection);
}
}
}
public CollectionViewSource CVS
{
get { return _CVS; }
set
{
if (_CVS != value)
{
_CVS = value;
RaisePropertyChanged(() => CVS);
}
}
}
public ICollectionView CVSView
{
get { return CVS.View; }
}
Here is where the CVS is initialized (in the window view-model's constructor). For now, the data collection is populated with a ton of random things (that's all that RandomizeData() does).
MyDataCollection = new ObservableCollection<Thing>();
RandomizeData();
CVS = new CollectionViewSource();
CVS.Source = MyDataCollection;
Here is the code for the [Filter] button's command:
//Clear any stale filter options and rebuild with the most current ones.
CVSView.Filter = null;
//IF THE FOLLOWING LINE IS UNCOMMENTED, THE ISSUE OCCURS.
//CVS.Filter -= new FilterEventHandler(SingleRandomFromCollectionFilter);
BuildCVSFilter();
The code for the [All] button literally just clears the filter:
CVSView.Filter = null;
The code for the [Random] button. I suspect the issue is coming from the handler added here.
//Clear any stale filter options and rebuild with the most current ones.
CVSView.Filter = null;
//IF THE FOLLOWING LINE IS UNCOMMENTED, THE ISSUE OCCURS.
//CVS.Filter -= new FilterEventHandler(SingleRandomFromCollectionFilter);
BuildCVSFilter();
//Only proceed if there are actually results in the filtered collection.
int resultsCount = CVSView.Cast<Thing>().Count();
if (resultsCount > 0)
{
//Point to a random thing in the filtered collection.
CVSView.MoveCurrentToPosition(random.Next(0, resultsCount));
_randomThing = CVSView.CurrentItem as Thing;
//Add another filter event that further constrains the collection to only contain the random thing.
CVS.Filter += new FilterEventHandler(SingleRandomFromCollectionFilter);
}
And here is the code for that BuildCVSFilter() I've been calling. I use this so that I can use multiple filters concurrently. The "FilterOption" strings are properties that are bound to the values of the UI controls.
if (!string.IsNullOrEmpty(FilterOption1))
{
CVS.Filter += new FilterEventHandler(Fitler1);
}
if (!string.IsNullOrEmpty(FilterOption2) && FilterOption2 != "ignore")
{
CVS.Filter += new FilterEventHandler(Fitler2);
}
if (!string.IsNullOrEmpty(FilterOption3))
{
CVS.Filter += new FilterEventHandler(Filter3);
}
Each filter that gets added this way only sets e.Accepted to false, if applicable, and leaves it alone if it's true.
Issue:
If I click on [Random] at all, it seems like the FilterEventHandler that gets added there does not go away. This makes it so that selecting [Filter] after [Random] won't work as expected, because it's only going to filter from the current collection of one thing. Additionally, selecting [Random] a second time seems to reuse the same random thing (instead of finding a new one). Interestingly enough, clicking [All] still works just fine. It shows everything.
When I go into those [Filter] and [Random] OnCommand methods and explicitly add a line to remove that SingleRandomFromCollectionFilter handler, everything works as expected.
Why would NameEntriesView.Filter = null; work to clear the filter on [All], but not on [Filter] or [Random]? Is there something about the CollectionViewSource and its implementation that I'm not fully understanding?

Related

Windows Form Combobox.Items.Clear() leaves empty slots

I am working on a Windows Form that has multiple comboboxes. Depending on what is chosen in the first combobox determines what items are filled into the second combobox. The issue I am running into is if I choose ChoiceA in ComboBox1, ComboBox2 is clear()ed, then is filled with ChoiceX, ChoiceY, and ChoiceZ. I then choose ChoiceB in ComboBox1, ComboBox2 is clear()ed, but there are no choices to add to ComboBox2, so it should remain empty. The issue is, after choosing ChoiceB, there's a big white box with three empty slots in ComboBox2. So, basically, however many items are cleared N, that's how many empty slots show up after choosing ChoiceB.
This might be a tad confusing, I hope I explained it well enough.
-- EDIT Adding Code, hope it helps clear things up. BTW, mainItemInfo is another "viewmodel" type class. It interfaces back into the form to make updates.
private void cmbType_SelectedIndexChanged(object sender, EventArgs e)
{
DropDownItem item = (DropDownItem)cmbType.SelectedItem;
if (!String.IsNullOrWhiteSpace(item.Text))
{
cmbBrand.Enabled = true;
btnAddBrand.Enabled = true;
mainItemInfo.FillBrands(new Dictionary<string, string> { { "Type", item.Text } });
mainItemInfo.SyncBrands(this);
}
}
public void FillBrands(Dictionary<string, string> columnsWhere)
{
// Clear list
Brands.Clear();
// Get data
StorageData storage = new StorageData(File.ReadAllLines(ItemsFilePath));
// Fill Brands
foreach (string type in storage.GetDistinctWhere(columnsWhere, "Brand"))
{
Brands.Add(type, new DropDownItem(type, type));
}
}
public void SyncBrands(IPopupItemInfo form)
{
form.ClearcmbBrand();
var brands = from brand in Brands.Keys
orderby Brands[brand].Text ascending
select brand;
foreach (var brand in brands)
{
form.AddTocmbBrand(Brands[brand]);
}
}
public void AddTocmbBrand(DropDownItem brand)
{
cmbBrand.Items.Add(brand);
}
public void ClearcmbBrand()
{
cmbBrand.Items.Clear();
}
Simply, you can add an item then clear the combobox again:
cmbBrand.Items.Clear();
cmbBrand.Items.Add(DBNull.Value);
cmbBrand.Items.Clear();
You should able to set the datasource of listbox2 to null to clear it, then set it again with the new data.
So, in pseudo-code, something like:
ItemSelectedInListBox1()
{
List futureListbox2Items = LoadOptionsBaseOnSelectedItem(item)
Listbox2.Datasource = null
Listbox2.Datasource = futureListBox2Items
}
That should refresh the list of items displayed in Listbox2 with no white spaces.
I was able to fix the extra space. I changed the Add and Clear methods to:
public void AddTocmbModel(DropDownItem model)
{
cmbModel.Items.Add(model);
cmbModel.DropDownHeight = cmbModel.ItemHeight * (cmbModel.Items.Count + 1);
}
public void ClearcmbModel()
{
cmbModel.Items.Clear();
cmbModel.DropDownHeight = cmbModel.ItemHeight;
}

How do I position a datagridview to the searched text input

Using Windows forms and linq to Sql, I bound a datagridview to Products Table, I added to the form 1 Textbox to input the searched text.
I wonder how to position the datagridview according to the text entered to find a given ProductName.
Here I do not want to filter rows, I only want to reposition datagrid after each character entered, the used code:
private void textBox1_TextChanged(object sender, EventArgs e)
{
var searchValue = textBox1.Text.Trim().ToUpper();
var qry = (from p in dc.Products
where p.ProductName.ToUpper().StartsWith(searchValue)
select p).ToList();
int itemFound = productBindingSource.Find("ProductName", searchValue);
productBindingSource.Position = itemFound;
}
The execution of code give the next error: System.NotSupportedException was unhandled at the ligne:
int itemFound = productBindingSource.Find("ProductName", searchValue);
Any idea please ?
The MSDN documentation for BindingSource has the answer:
The Find method can only be used when the underlying list is an
IBindingList with searching implemented. This method simply refers the
request to the underlying list's IBindingList.Find method. For
example, if the underlying data source is a DataSet, DataTable, or
DataView, this method converts propertyName to a PropertyDescriptor
and calls the IBindingList.Find method. The behavior of Find, such as
the value returned if no matching item is found, depends on the
implementation of the method in the underlying list.
When you call this method on a BindingSource whose underlying data source does not implement IBindingList then you see the exception (thrown by the default implementation of IBindingList.FindCore:
System.NotSupportedException: The specified method is not supported.
You don't show what you bind the binding source to but clearly it doesn't implement this method.
Annoyingly, BindingList<T> the recommended list type to use for your data source does not provide a FindCore implementation.
If you are using BindingList you will need to create your own custom type. Here is the code for an absolutely bare bones implementation of a BindingList that supports find:
public class FindableBindingList<T> : BindingList<T>
{
public FindableBindingList()
: base()
{
}
public FindableBindingList(List<T> list)
: base(list)
{
}
protected override int FindCore(PropertyDescriptor property, object key)
{
for (int i = 0; i < Count; i++)
{
T item = this[i];
if (property.GetValue(item).Equals(key))
{
return i;
}
}
return -1; // Not found
}
}
You can do lots with your own implementations of BindingList such as supporting sorting. I've left my answer as just the minimum to support the find method. Search for SortableBindingList if you want to know more.
To use this class do something like this:
var qry = (from p in dc.Products
where p.ProductName.ToUpper().StartsWith(searchValue)
select p).ToList();
FindableBindingList<YourType> list = new FindableBindingList<YourType>(qry);
dataGridView1.DataSource = list;

How to improve performance of Search as you type?

I am trying to implement a Search as you type functionality ( like the one in the search in the default email app ) - I have a listbox with say 50 items - each item bound to a class object which has string fields ... I wish to search and display items which have the text in the search box in one of their string fields - this as the user keys in to the textbox ... tried a couple of approaches -->
1 >> Using a CollectionViewSource
- Bound a CollectionViewSource to All the items from the DB
- Bound the List Box to a CollectionViewSource
- Setting the filter property of the CollectionViewSource - to whose function which searchs for the text in the Search box in the items and set the e.Accepted - on every keyup event
- Filtering works fine but its slow with 50 items :( - guess cos of filter taking each item and checking if to set e.Accepted property to true
.... One DB call on load but seems to be a lot of processing to decide which element to disply in the filer by the CollectionViewSource
2 >> Filter # DB level
- on keyup - sending the text in the search box to the ViewModel where a function returns an ObservableCollection of objects which has the search string
- ObservableCollection is bound to the listbox
.... Not much processing # the top layer but multiple DB calls on each Keypress - still slow but just a tad faster than Approach One
Is there any other approch you would recommend ? or any suggestions to further optimize the above mentioned approaches? - any tweak to give smooth functioning for the search?
First time into mobile dev :) ... Thanx in advance :)
You can optimize the search process further by delaying the search action by x milliseconds to allow the user to continue the writing of the search criteria. This way, each new typed char will delay the search action by xxx milliseconds till the last char where the action is triggered, you'll get a better performance using just one call instead of say 10 calls. Technically speaking, you can achieve this by using a Timer class.
Here you'll find a sample source code example that shows u how to implement this.
private void txtSearchCriteria_TextChanged(object sender, TextChangedEventArgs e)
{
if (txtSearchCriteria.Text == "Search ...")
return;
if (this.deferredAction == null)
{
this.deferredAction = DeferredAction.Create(() => ApplySearchCriteria());
}
// Defer applying search criteria until time has elapsed.
this.deferredAction.Defer(TimeSpan.FromMilliseconds(250));
}
private DeferredAction deferredAction;
private class DeferredAction
{
public static DeferredAction Create(Action action)
{
if (action == null)
{
throw new ArgumentNullException("action");
}
return new DeferredAction(action);
}
private Timer timer;
private DeferredAction(Action action)
{
this.timer = new Timer(new TimerCallback(delegate
{
Deployment.Current.Dispatcher.BeginInvoke(action);
}));
}
public void Defer(TimeSpan delay)
{
// Fire action when time elapses (with no subsequent calls).
this.timer.Change(delay, TimeSpan.FromMilliseconds(-1));
}
}
public void ApplySearchCriteria()
{
ICollectionView dataView = this.ViewSource.View;
string criteria = this.txtSearchCriteria.Text;
string lowerCriteria = criteria.ToLower();
using (dataView.DeferRefresh())
{
Func<object, bool> filter = word =>
{
bool result = word.ToString().ToLower().StartsWith(lowerCriteria);
return result;
};
dataView.Filter = l => { return filter.Invoke(l); };
}
this.lstWords.ItemsSource = dataView;
if (this.lstWords.Items.Count != 0)
{
this.lblError.Visibility = Visibility.Collapsed;
}
else
{
this.lblError.Visibility = Visibility.Visible;
}
}

Silverlight data binding breaks when there is no debugger attached

Ok, a bit of a strange one - and it's probably something simple as I'm pretty new to Silverlight!
I have an object with the following property:-
private int targetID = NULL_TARGET_VALUE;
[Display(Name="Target", Order=1)]
[Required]
public int TargetID
{
get
{
return targetID;
}
set
{
if (this.targetID != value)
{
this.ValidateProperty("TargetID", value);
this.targetID = value;
this.RaisePropertyChanged("TargetID");
}
}
}
This object is created using the DataForm from the toolkit. I use the AutoGeneratingField event to change the item to a combo box drop down with the code below:
if (e.PropertyName == "TargetID")
{
ComboBox target = new ComboBox() { DisplayMemberPath = "Title", SelectedValuePath = "ItemID" };
target.ItemsSource = TaskManager.Manager.GanttItemSource;
var selectedItem = TaskManager.Manager.GanttItemSource.FirstOrDefault(p => p.ItemID == ParentTargetID);
target.SelectedItem = selectedItem;
e.Field.ReplaceTextBox(target, ComboBox.SelectedValueProperty, binding => binding.Converter = new TargetNullValueConverter());
break;
}
This does result in a drop down as I would expect. On my save button event I have this code:
if (registerForm.ValidateItem())
{
this.task.Save();
}
If the debugger is attached to the silverlight project this works great. If it's not then ValidateItem returns false as it thinks I have added an invalid target ("Input is not in a correct format" is the exact validation error I get).
Any ideas really appreciated! (BTW Just to confirm this happens in both release and debug build modes, simply attaching or removing a debugger causes this to occur)
Dammit, the issues was down to this line in the autogeneratingfield event:-
e.Field.ReplaceTextBox(target, ComboBox.SelectedValueProperty, binding => binding.Converter = new TargetNullValueConverter());
This allows it to work
e.Field.ReplaceTextBox(target, ComboBox.SelectedValueProperty);
Which makes sense because I'm dealing with ints not objects. Still don't know why it works with the debugger attached however!

How can I get items from current page in PagedCollectionView?

I've got my objects in PagedCollectionView bound to DataGrid and DataPager.
var pcView = new PagedCollectionView(ObservableCollection<Message>(messages));
How can I easily get items from current page in PagedCollectionView from my ViewModel? I wish there were something like this:
var messagesFromCurrentPage = pcView.CurrentPageItems; // error: no such a property
There are properties like SourceCollection, PageIndex and Count but I don't find them useful in this case. What am I missing here?
If you want to get select items you can just use Linq to do it.
var items = pcView.Where(i => i.SomeCondition == true);
Make sure you add a using statement for System.Linq.
Edit: Whenever I have a question as to what is really going on I just look at the code using Reflector (or ILSpy). In this case here is the relevant code inside GetEnumerator() which is how the Select or Where gets the items in the list:
List<object> list = new List<object>();
if (this.PageIndex < 0)
{
return list.GetEnumerator();
}
for (int i = this._pageSize * this.PageIndex; i < Math.Min(this._pageSize * (this.PageIndex + 1), this.InternalList.Count); i++)
{
list.Add(this.InternalList[i]);
}
return new NewItemAwareEnumerator(this, list.GetEnumerator(), this.CurrentAddItem);
So you can see how it is returning only the items in the current page from this code.

Resources