How do I update a single item in an ObservableCollection class? - c#

How do I update a single item in an ObservableCollection class?
I know how to do an Add. And I know how to search the ObservableCollection one item at a time in a "for" loop (using Count as a representation of the amout of items) but how do I chage an existing item. If I do a "foreach" and find which item needs updating, how to I put that back into the ObservableCollection>

You can't generally change a collection that you're iterating through (with foreach). The way around this is to not be iterating through it when you change it, of course. (x.Id == myId and the LINQ FirstOrDefault are placeholders for your criteria/search, the important part is that you've got the object and/or index of the object)
for (int i = 0; i < theCollection.Count; i++) {
if (theCollection[i].Id == myId)
theCollection[i] = newObject;
}
Or
var found = theCollection.FirstOrDefault(x=>x.Id == myId);
int i = theCollection.IndexOf(found);
theCollection[i] = newObject;
Or
var found = theCollection.FirstOrDefault(x=>x.Id == myId);
theCollection.Remove(found);
theCollection.Add(newObject);
Or
var found = theCollection.FirstOrDefault(x=>x.Id == myId);
found.SomeProperty = newValue;
If the last example will do, and what you really need to know is how to make things watching your ObservableCollection be aware of the change, you should implement INotifyPropertyChanged on the object's class and be sure to raise PropertyChanged when the property you're changing changes (ideally it should be implemented on all public properties if you have the interface, but functionally of course it really only matters for ones you'll be updating).

You don't need to remove item, change, then add. You can simply use LINQ FirstOrDefault method to find necessary item using appropriate predicate and change it properties, e.g.:
var item = list.FirstOrDefault(i => i.Name == "John");
if (item != null)
{
item.LastName = "Smith";
}
Removing or adding item to ObservableCollection will generate CollectionChanged event.

Here are Tim S's examples as extension methods on top of the Collection Class:
CS with FirstOrDefault
public static void ReplaceItem<T>(this Collection<T> col, Func<T, bool> match, T newItem)
{
var oldItem = col.FirstOrDefault(i => match(i));
var oldIndex = col.IndexOf(oldItem);
col[oldIndex] = newItem;
}
CS with Indexed Loop
public static void ReplaceItem<T>(this Collection<T> col, Func<T, bool> match, T newItem)
{
for (int i = 0; i <= col.Count - 1; i++)
{
if (match(col[i]))
{
col[i] = newItem;
break;
}
}
}
Usage
Imagine you have this class setup
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
You can call either of the following functions/implementations like this where the match parameter is used to identify the item you'd like to replace:
var people = new Collection<Person>
{
new Person() { Id = 1, Name = "Kyle"},
new Person() { Id = 2, Name = "Mit"}
};
people.ReplaceItem(x => x.Id == 2, new Person() { Id = 3, Name = "New Person" });
VB with Indexed Loop
<Extension()>
Public Sub ReplaceItem(Of T)(col As Collection(Of T), match As Func(Of T, Boolean), newItem As T)
For i = 0 To col.Count - 1
If match(col(i)) Then
col(i) = newItem
Exit For
End If
Next
End Sub
VB with FirstOrDefault
<Extension()>
Public Sub ReplaceItem(Of T)(col As Collection(Of T), match As Func(Of T, Boolean), newItem As T)
Dim oldItem = col.FirstOrDefault(Function(i) match(i))
Dim oldIndex = col.IndexOf(oldItem)
col(oldIndex) = newItem
End Sub

This depends on what type of object it is.
If it is an ordinary C# class, just change the object's properties. You don't have to do anything to the collection. The collection holds a reference to the object which is the same even if the object's properties changes. A change on the object won't trigger the change notification for the collection itself, as the collection has not really change, just one of the objects in it.
If it is an immutable C# class (such as string), a struct or another value type you have to remove the old value and add the new one.

Related

Update list without modifying the parent

I have an in memory list, from which I get some rows and update a field. I can't understand why the parent list is also updated. I used ToList(), Range(), new List<type>(filteredRows), nothing works. I don't want to create another list and just push item by item, because the parent is huge and can affect the performance.
private readonly IServiceScopeFactory scopeFactory;
private List<Row> rows= new List<Row>();
public InMemoryData(IServiceScopeFactory scopeFactory)
{
this.scopeFactory = scopeFactory;
}
the original rows looks like below:
{ Name: 'Team A', Position: 1, CountryId: 1},
{ Name: 'Team B', Position: 2, CountryId: 2},
{ Name: 'Team C', Position: 3, CountryId: 1},
{ Name: 'Team D', Position: 4, CountryId: 1}
The problem is in the following method:
public List<Row> GetFiltereRows(int? countryId = null) {
if (countryId != null) {
var filteredRows = rows
.Where(x => x.CountryId == (int)countryId)
.ToList();
var position = 0;
return filteredRows.Select(x => {
// here, also the entity "x" in the parent list is updated
x.Position = ++position;
return x;
}).ToList();
}
return rows;
}
When I filter the data by CountryId 1, I want that Team C to have Position 2 and Team D to have Position 3 (to recalculate positions only for selected country).
I think I need to set the rows list as readonly, or to create clones from it.
I can't understand why for another case, the clone works:
var filteredRows = rows;
if (countryId != null) {
filteredRows = filteredRows.Where(x => x.CountryId == countryId ).ToList();
}
here, even if the filteredRows is declared as rows, this will not update the original list, filtering it. I know something about immutability from another languages, due of that I'm thinking that I need to do theoriginal list immutable, to use only copies of it in another methods. But also, somethimes I need to refresh the original list, so should still be able to update it (or recreate).
I used ToList(), Range(), new List(filteredRows), nothing works.
new List<Type>(filteredRows) will create another reference pointing to the same list, it won't create a new list and clone the items in the original (parent) list.
.ToList()? Same thing, Actually, take a look at .ToList()'s implementation:
public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source) => source != null ? new List<TSource>(source) : throw Error.ArgumentNull(nameof (source));
It's just the same as new List<Type>(filteredRows), but throws an exception if filteredRows is null!
"// here, also the entity "x" in the parent list is updated" this is normal
As stated, you are not deep-cloning the original items, so any modification to the filtered item is a direct modification to the original item.
I can think of two choices to overcome this issue:
After filtering rows by countryId, you can tell how long is filteredRows, so maybe it won't affect the performance if you deep-clone it
return filteredRows.Select(x => {
var xClone = new Row(x); // deep clone
xClone.Position = ++position;
return xClone;
}).ToList();
Or, you would write new function to reverse the edits made to the original list at some point after using the returned value from GetFiltereRows..
return filteredRows.Select(x => {
x.OriginalPosition = x.Position;
x.Position = ++position;
return x;
}).ToList();
Usage:
var filteredRows = GetFiltereRows(rows);
// use the filteredRows
RefreshRows();
void RefreshRows(){
foreach(var item in rows) {
item.Position = item.OriginalPosition;
item.OriginalPosition = -1;
}
}
Even if you were to create a new list and push item by item, you would have the same issue.
This is because each row (your object with Name, Position, and CountryId) is a reference type as well. So even if you make a brand new list, that new list will still point to the same object in the other list. This is why you see both lists being updated, even after you successfully made a new list.
Your code would work as you intended if your list was composed of a value type, such as List<int>.
The only way I know how to fix this is to make a Row class and have it conform to ICloneable, like this:
class Row: ICloneable
{
public String Name;
public int Position;
public int CountryId;
public object Clone()
{
return this.MemberwiseClone();
}
}
Unfortunately, you would have to call Clone() on each row in your list, which will affect performance.
If anyone else has a better way, feel free to add on.

Group, Sort, and then Merge to a Observable Collection?

I have a List of Items that have a "DisplayOrder" property. This can either have NULL or int value. If it has int value, it has priority and should be in the 1st group of the Observable Collection. Items in the 1st group are also sorted by DisplayOrder.
If it is NULL, then it belongs to the 2nd group, sorted alphabetically.
The 1st group and 2nd group are then combined for a Main Items Collection Observable Collection which I bind to a ListView.
This is my current code though I am worried if there is a much optimal way of doing it.
var MainItemCollection = new ObservableCollection<MainItemViewModel>();
var ItemsWithProperty = new ObservableCollection<MainItemViewModel>();
var ItemsWithNullProperty = new ObservableCollection<MainItemViewModel>();
foreach (var item in DataObject.MainItems)
{
if (item.DisplayOrder == null)
ItemsWithNullProperty.Add(new MainItemViewModel(item));
else
ItemsWithProperty.Add(new MainItemViewModel(item));
}
ItemsWithProperty = new ObservableCollection<MainItemViewModel>(ItemsWithProperty.OrderBy(c => c.DisplayOrder));
ItemsWithNullProperty = new ObservableCollection<MainItemViewModel>(ItemsWithNullProperty.OrderBy(c => c.Title));
//Add those with priorities first sorted by DisplayOrder 1,2,3,4
foreach (var c in ItemsWithProperty)
{
MainItemCollection.Add(c);
}
//Add those without priority sorted Alphabetically
foreach (var c in ItemsWithNullProperty)
{
MainItemCollection.Add(c);
}
Thank you!
Get the items with DisplayOrder=null & order them by Title:
ItemsWithNullProperty=DataObject.MainItems.Where(x=>x.DisplayOrder==null).OrderBy(o=>o.Title).ToList();
Get the items with DisplayOrder(all items except the above query) & order them by DisplayOrder:
ItemsWithProperty= DataObject.MainItems.Except(ItemsWithNullProperty).OrderBy(o=>o.DisplayOrder).ToList();
Fill the data in MainCollection:
var allItems = MainItemCollection.Concat(ItemsWithProperty)
.Concat(ItemsWithNullProperty)
.ToList();
When doing things like this, you don't need all those intermediate ObservableCollections - you can use the appropriate data structures like array, list, dictionary, hash set etc. or Linq queries. In this particular case, the whole procedure can be reduced to something like this
var MainItemCollection = new ObservableCollection<MainItemViewModel>(DataObject.MainItems
.OrderBy(item => item.DisplayOrder ?? int.MaxValue)
.ThenBy(item => item.DisplayOrder == null ? item.Title : string.Empty)
);
I feel that this is a pretty common scenario.
There is a sorted ObservableCollection bound to some XAML UI, and once more data is available UI needs to be updated without full refresh.
Whenever new ObservableCollection is created like in suggestions above, all items will be rebound and therefore UI fully updated.
I'm surprised that there are no library methods to achieve this. Here is the solution I've came up with. Hope someone might find it useful.
public static class ObservableCollectionExtensions
{
public static void MergeSortedListIntoSortedObservableCollection<T>(this ObservableCollection<T> destination, IList<T> list, Func<T, T, int> compareFunc)
{
int dest_index = 0;
int list_index = 0;
while (list_index < list.Count)
{
if (dest_index >= destination.Count)
{
destination.Add(list[list_index]);
list_index++;
dest_index++;
}
else
{
if (compareFunc(destination[dest_index], list[list_index]) > 0)
{
destination.Insert(dest_index, list[list_index]);
list_index++;
dest_index++;
}
else
{
dest_index++;
}
}
}
}
}

Iterate through the items in a Listbox

I have a ListBox (sortedListBox) which I have populated like this by the items in a Combobox (allItemsComboBox):
int index = sortedListBox.FindString(allItemsComboBox.Text, -1);
if (index == -1)
{
var item=new { Text = allItemsComboBox.Text , Value = allItemsComboBox.Value};
sortedListBox.Items.Add(item);
}
The DisplayedMember of sortedListBox is "Text" and ValueMember of it is "Value".
Now I want to iterate through all items in the ListBox and get its values:
public static string ListBoxToString(ListBox lb)
{
List<string> values = new List<string>();
for (int i = 0; i < lb.Items.Count; i++)
{
values.Add(lb.Items[i].ToString());
}
string result = String.Join(",", values);
return result;
}
In this line: values.Add(lb.Items[i].ToString()); I get:
{ Text = "Size" , Value = "cte1.Size"}
I just want to have the value , which is "cte1.Size"
How can I iterate through the items in the ListBox and get the ValueMember of these?
I don't know that there's any way to ask the ListBox to evaluate the ValueMember for you in that way... and because you're using an anonymous type, it becomes harder to get the value.
Options:
Use a named type instead, and cast each item to that
Use dynamic typing
For example:
public static string ListBoxToString(ListBox lb)
{
var values = lb.Items
.Cast<dynamic>()
.Select(x => x.Value.ToString());
return string.Join(",", values);
}
Dynamic typing provides the most immediate fix, but I'd strongly encourage you to consider using a custom type. (It needn't take more than a few lines to write.)
There are two problems with your approach:
1.) The ListBox stores items as a collection of objects which means accessing them with listBox.Items[idx] will only give you back an object and not the actual type. You could get around that with casting it to the appropriate type but it won't work in your case because of the next point.
2.) You create your items as anonymous objects with var item = new { ... }. You can't cast to this kind of type. You could use the dynamic keyword to get around that but I wouldn't do that as you lose type safety.
What you could do is create a simple class for the date you want to store and use that instead of an anonymous type:
class MyData
{
public string Text { get; set; }
public string Value { get; set; }
}

Assigning to List to another List previous list automatically changes

I have two lists. When I assign List1 to List2 and I update List1, List2 automatically updates too. List2 shouldn't update. Why does this happen?
Here is my code:
public List<TrialBalance> TBal { get; set; }
public List<TrialBalance> PrevTBal { get; private set; }
if (this.PrevTBal == null)
{
this.PrevTBal = this.TBal;
}
for (int x = 0; x < this.TBal.Count; x++)
{
this.TBal[x].Balance = this.TBal[x].Balance + adjustments;
}
You are only assigning the references, not creating a copy of either the list or the items in the list.
You should create a new list and add all the items to it.
this.PrevTBal = new List<TrialBalance>(this.TBal.Select(b => clone(b));
When you assign a List<T>, you're copying the handle to the actual list in memory, which means the same list instance is being referenced by both variables.
In order to avoid this, you'd need to clone the list itself. In this case, that likely means needing to do two things - first, make a way to clone TrialBalance, then clone the list too:
// This assumes a TrialBalance.Clone() method which returns a new TrialBalance copy
this.PrevTBal = this.TBal.Select(tb => tb.Clone()).ToList();
Replace
if (this.PrevTBal == null)
{
this.PrevTBal = this.TBal;
}
by:
if (this.PrevTBal == null)
{
this.PrevTBal = this.TBal.ToList();
}
This way you're actually creating a copy of it instead of just refering it.

Finding a specfic int in a List

I have a list which is created from a Class:
class Vara
{
public int streckKod { get; set; }
public string artNamn { get; set; }
}
And the list looks like this:
List<Vara> minaVaror = new List<Vara>();
And I add to the list with this line:
minaVaror.Add(new Vara() {streckKod = inputBox1, artNamn = textBox2.Text });
But what I'm stuck at is how I can find a specific int within the list. Let's say I'm searching for the item in my list holding the number 293 in the variable streckKod.
I've tried using .IndexOf function but I haven't gotten it to work properly.
Also would it be possible to get the item number that the specific number is located in?
If you want to find all items whose streckKod value is 293 use Where
var items = minaVaror.Where(i => i.streckKod == 293);
If interested in only first item use FirstOrDefault -
var item = minaVaror.FirstOrDefault(i => i.streckKod == 293);
FirstOrDefault will return null in case no item exist in collection with value 293.
Make sure you add namespace System.Linq in your class to use these LINQ extension methods.
Use Linq
minaVarror.Find(item=>item.strekKod == 293);
Adding to Rohit Vats answer...
When you have found your item, with either Where or FirstOrDefault you can get the index by doing:
var item = minaVaror.FirstOrDefault(i => i.streckKod == 293);
// Get index of Vara (item) with streckKod = 293
int index = minaVaror.IndexOf(item);
As IndexOf returns the position of an exact item (Vara) within the list minaVaror

Categories