Filtering a ObservableCollection by user input - c#

I have an ObservableCollection of about 1000 objects that needs to be filtered (searched) by the end user. The user must be able to search by name or employee id. The List Control consumes FilteredEmployees and Employees is loaded up with everything on page load.
I currently have it set up as such:
public ObservableCollection<EmployeeServicesData> Employees { get; set; }
public ObservableCollection<EmployeeServicesData> FilteredEmployees { get; set; }
internal void FilterEmployee(string searchText, bool isByName)
{
if (searchText.Length > 0)
{
IEnumerabe<EmployeeServicesData> filter;
if (isByName)
filter = Employees.Where(x => x.Name.Length >= searchText.Length).Where(x => x.Name.Substring(0, searchText.Length) == searchText.ToUpper());
else
filter = Employees.Where(x => x.EmployeeNumber.ToString().Length > searchText.Length).Where(x => x.EmployeeNumber.ToString().Substring(0, searchText.Length) == text);
foreach (EmployeeServicesData employee in filter)
FilteredEmployees.Add(employee);
}
}
Sanitation is handled before this method.
This doesn't smell very efficent. Should I use two methods for this, or is there a better way to handle filtering?
I'd like to keep Employees at an unchanged state so I can repopulate FilteredEmployees to the full list without hitting the DB again.

I know this is an old post but I was using it to help me with the filtering aspect and noticed that SlipFish was creating the ObservableCollection by looping round the IEnumerable collection.
As the ObservableCollection constructor accepts an IEnumerable collection the ObservableCollection could be created like this:
FilteredEmployees = new ObservableCollection<EmployeeServicesData>(filter);

It looks like you're trying to see if searchText is contained in the Employee name, or in the Employee Number.
You could do this instead:
x.Name.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0
x.EmployeeNumber.ToString().IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0
Or you could use StartsWith instead of IndexOf.
Edit: Another problem with List Controls with large amounts of data in them is that it takes a long time to render. So if you have it unfiltered when you start and Silverlight or WCF or whatever has to render all 1000 into the control even though you don't see all of them, it can take a little bit of time. Silverlight 3 has UI Virtualization, which would probably be the best optimization you could do here.

Take a look at this post for a filtered observable collection.

You can this through the PagedCollectionView.
Check out this blog post for an example: http://blogs.msdn.com/b/avip/archive/2009/10/30/real-time-list-filtering-with-silverlight-mvvm-and-pagedcollectionview.aspx

Related

Adding a sum to a list instead of a count

I'm using ASP.Net Core 3.1 to develop a web app. We need to return a list of values to a View. The list includes counts and sums of data. We have created a ViewModel to help. It looks like this:
public class ObjectCountViewModel
{
[DisplayName("Description")]
public string Description { get; set; }
[DisplayName("Count")]
public decimal Count { get; set; }
}
We created a list in the Controller to return the values. It looks like this:
List<ObjectCountViewModel> objectCounts = new List<ObjectCountViewModel>();
Next we added values to the list like this:
int itemsToCount = objects.Where(e => e.ObjectItems.Where(ep => ep.ObjectItemType.Description.Contains("ItemToCount") && ep.ObjectItemSelctionType.Description.Contains("Taken")).Count()>0).Count();
objectCounts.Add(new ObjectCountViewModel() { Description = "Items Counted", Count = itemsToCount });
This code works great! But we also need to generate a sum. this will be used to count items with a decimal I can't get a sum to work. Here is one of the solutions I have tried:
decimal itemToSum = objects.Where(e => e.ObjectItems.Where(ep => ep.ObjectItemType.Description.Contains("ItemToSum") && ep.ObjectItemSelectionType.Description.Contains("Taken") && ep.ObjectValueAmount>0).Sum()>0).Sum();
objectCounts.Add(new ObjectCountViewModel() { Description = "Items Taken Sum", Count = itemToSum });
I have received a wide variety of errors. The current one is: 'IEnumerable' does not contain a definition for 'Sum' and the best extension method overload 'ParallelEnumerable.Sum(ParallelQuery)' requires a receiver type of 'ParallelQuery,decimal>.
What am I doing wrong? What should my query look like for a sum?
If you have a list of lists, where you want to count all lists, then use listsOfList.SelectMany(x=>x).Count().
If you have a list of decimals, where you want a sum of all decimals, then use listsOfDecimals.Sum().
If you have a list of lists of decimals, where you want a sum of all decimals, then use listsOfListOfDecimals.SelectMany(x=>x).Sum().
I found the answer thanks to Heretic Monkey and Intellisense. I had to create a new object with the value I'm trying to sum and then filter to only select from ones that met my criteria. Then, I separated the Select statement from the Where Clause as Heretic Monkey said. Intellisense suggested I put (decimal) in front of the whole thing, and it worked! Here is my final code for this problem.
decimal itemToSum = (decimal)Objects.Where(ep => ep.RelatedObjectType.Description.Contains("Description") && ep.DifferentRelatedObjectType.Description.Contains("Description")).Select(ep => ep.itemToSum).Sum();

uwp limit the number of items in AdvancedCollectionView

I have two advanced collection views from windows community toolkit and both of them are bound to same ObservableCollection with different filters and sorting, basically in one of them I need to show just the recent and limited number of items. How can I achieve that?
PeoplePrivate = new ObservableCollection<Person>();
var People = new AdvancedCollectionView(PeoplePrivate, true) { Filter = x => true };
People.SortDescriptions.Add(new SortDescription(nameof(Person.Name), SortDirection.Ascending));
var RecentPeople = new AdvancedCollectionView(PeoplePrivate, true) { Filter = x => true };
RecentPeople.SortDescriptions.Add(new SortDescription(nameof(Person.Modified), SortDirection.Descending));
As you can see in the code above recentPeople should only show recent 20 people according to the modified date. There doesn't seem to be any property to set max size on the advancedCollection view or do anything like "Take(20)".
I tried to return a new advancedCollection by creating a IEnumerable first with Take(20) but that doesn't look the right way because I need to keep it linked to the same ObservableCollection.
view or do anything like "Take(20)" I tried to return a new advancedCollection by creating a IEnumeralbe first with Take(20)
Currently AdvancedCollectionView has not provide this method to get recent number items. But you could remove all the items except top 20 of the source.
public static class AdvancedCollectionViewEx
{
public static void GetTopRang(this AdvancedCollectionView acv, int Range)
{
do
{
var LastIndex = acv.Source.Count - 1;
acv.Source.RemoveAt(LastIndex);
} while (acv.Source.Count > Range);
}
}
Usage
RecentPeople.GetTopRang(20);
I like the WPF answer provided here and use a Binding Converter to chop the end-result of the collection view when it's bound to the ListView. Then it should get updated when the collection changes and re-filter?

Does Linq OrderBy Not Sort Original Collection?

I am using Linq To Sql as my database layer for the first time and I have run into an issue. Basically, I have a form that allows users to create predefined jobs. A predefined job can have many predefined job items. For example, a predefined job might be something like an oil change. The predefined job items would be oil, labor, etc. In my database PredefinedJobs is a table and PredefinedJobItems is another table with a foreign key back to PredefinedJobs. I have a form for adding predefined jobs that has the Linq-to-Sql class backing the form as a class variable. There is a ListView on the form that displays all of the jobs items. A new feature has required me to track the position of an item in the ListView. For example, if my item ListView looks like below, note the order:
Qty Name Desc ItemOrder
4 Oil Penzoil 0
1 Labor 1
Because the items are added via a child form I do not want to provide access to the ListView. So, I created the method below in an attempt to both create the ItemOrder and sort the collection on the PredefinedJob Linq to Sql object. It does not appear that the OrderBy function on the List actually sorts the collection on the PredefinedJob. What would be the best way to maintain order on the Linq to Sql collection (i.e. PredefinedJob.fkJobItems)? Or, would it be better to just pass a reference to my ListView into the child form that adds the items to the jobs where I have access to the selectedIndex?
private SortAndOrderItems(List<PredefinedJobsItem> items)
{
var nextItemOrderNumber = items.Max(max => max.ItemOrder) + 1;
foreach (var item in items)
{
if (item.ItemOrder == null)
{
item.ItemOrder = nextItemOrderNumber;
nextItemOrderNumber++;
}
}
items.OrderBy(i => i.ItemOrder).ToList();
}
OrderBy creates a new query, that will, when executed, not alter your original list.
Why not just the Sort method of the List?
items.Sort((a, b) => a.ItemOrder.CompareTo(b.ItemOrder));
I think you were looking for List<>.Sort
class Cmp : IComparer<PredefinedJobsItem>
{
public int Compare(PredefinedJobsItem x, PredefinedJobsItem y)
{
return x.ItemOrder.CompareTo(y.ItemOrder);
}
}
var comparison = new Cmp();
items.Sort(comparison);

How do I persist checked rows on a grid that is populated from Cache instead of Session?

I get a large list of data to populate into a jqGrid on my clientside.
List<MyData> lotsOfRecords = getData();
Which I then store in cache, since a lot of people will be using it:
Cache["SharedData"].Add(lotsOfRecords);
This grid allows users to check records for processing. I want to persist which records are checked as a user sorts, filters, pages, etc.
My first thought was to add a property bool Selected { get; set; } to the MyData object, and toggle it whenever someone checks a field. Obviously, that won't work since this is a shared cache. I don't want Joe User checking things that Bill User didn't want checked.
Next idea was to store a Dictionary<int, bool> in session, that maps the id of a record to the checked status. This wasn't bad, but since there is no easy way to combine objects in .NET, I don't see a clean way to send that down to my grid without a clunky anonymous object:
return lotsOfRecords.Select(record => {
record.Id,
record.Name,
...
myDictionary[record.Id] // true/false for checked
};
That would be a solution, but I'm hoping there is a cleaner design pattern considering I have a lot of fields in my object and use it in a similar way across a few pages. Any suggestions are welcome. Thanks in advance!
Btw, my current environment is ASP.NET MVC 3 with jQuery/UI and jqGrid.
You may be caching the list, but the selections will need to be user-specific. I would suggest building a list of the selected indices each time the page is posted back, and store the list session.
Here's a function that we're using to remember selections as the user pages through results:
/// <summary>
/// Iterates through items in the grid and updates the selected vendor
/// list with any selections or deselections on the current page
/// </summary>
private void UpdateSelectedItems()
{
var selectedVendors = new List<int>();
foreach (GridItem Item in grdVendors.Items)
{
if (Item is GridDataItem)
{
int VendorID = (int)((GridDataItem)Item).GetDataKeyValue("SupplierID");
if (Item.Selected)
{
if (!selectedVendors.Contains(VendorID))
selectedVendors.Add(VendorID);
continue;
}
selectedVendors.Remove(VendorID);
}
}
}
I'm not sure why you think "combining" objects is tough. You can simply do this:
public class SelectableDataObject
{
public SelectableDataObject(MyDataObject obj)
{
this.DataObject = obj;
}
public MyDataObject DataObject { get; private set; }
public bool Selected {get;set;}
}
Then you can just do this:
return lotsOfRecords.Select(record => {
return new SelectableDataObject(record){Selected = myDictionary.ContainsKey(record.Id)}
};
Alternatively, in your view model you can have the list of objects and the dictionary as two separate properties, and when you iterate the list of objects in your view to populate your grid, you can check the dictionary if the Id exists and check/uncheck based on that. This way is a bit more clunky, but should work.
Either way, I think your dictionary idea is perfectly fine, the only thing I'd do different is just store the Id's of the ones that are selected, that way you only store a subset for each user.

How to create view model without sorting collections in memory

I have a view model (below).
public class TopicsViewModel
{
public Topic Topic { get; set; }
public Reply LastReply { get; set; }
}
I want to populate an IQueryable<TopicsViewModel> with values from my IQueryable<Topic> collection and IQueryable<Reply> collection. I do not want to use the attached entity collection (i.e. Topic.Replies) because I only want the last reply for that topic and doing Topic.Replies.Last() loads the entire entity collection in memory and then grabs the last one in the list. I am trying to stay in IQueryable so that the query is executed in the database.
I also don't want to foreach through topics and query replyRepository.Replies because looping through IQueryable<Topic> will start the lazy loading. I'd prefer to build one expression and have all the leg work done in the lower layers.
I have the following:
IQueryable<TopicsViewModel> topicsViewModel = from x in topicRepository.Topics
from y in replyRepository.Replies
where y.TopicID == x.TopicID
orderby y.PostedDate ascending
select new TopicsViewModel { Topic = x, LastReply = y };
But this isn't working. Any ideas how I can populate an IQueryable or IEnumerable of TopicsViewModel so that it queries the database and grabs topics and that topic's last reply? I am trying really hard to avoid grabbing all replies related to that topic. I only want to grab the last reply.
Thank you for any insight you have to offer.
Since nobody is answering I am going with a foreach solution for now. I figure foreaching through the topics that are eventually going to be lazy loaded anyway is far better than populating a collection of replies just so I can access the last object in the collection.
Here is what I did for now:
List<TopicsViewModel> topicsViewModelList = new List<TopicsViewModel>();
foreach (Topic topic in topics)
{
Reply lastReply = replyRepository.GetRepliesBy_TopicID(topic.TopicID).OrderBy(x => x.PostedDate).LastOrDefault();
topicsViewModelList.Add(new TopicsViewModel
{
Topic = topic,
LastReply = lastReply
});
}
I'm just loading my IQueryable<Topics> first, then looping through the final results (so as to ensure that proper paging of the data is done before looping) and loading in the last reply. This seems to avoid ever populating a collection of replies and instead grabs only the last reply for each topic.
from r in replies
group r by new { r.TopicId } into g
select new
{
TopicId = g.Key.TopicId,
LastReply = g.Max(p => p.PostedDate)
}

Categories