简体   繁体   中英

Update items in a ObservableCollection when Collection is changed

Question: How can I update all items within an ObservableCollection when the CollectionChanged-Event is raised?

I search a lot but not find any suitable question with an answer for this. Maybe I searched wrong or my code is crap at all... ;-)

Background: I got a list of items from the user and need to check them in some criteria. One of the criteria is that one attribut needs to be gapless within the submitted items. This attribut is not directly visible to the user, so I cannot just throw him back to give me correct items. That is because I show him a Datagrid with the information which items are ok and which not. Now he is be able to delete items by selecting them and press del (I cannot decide which one should be deleted) so that finally all items in the list a valid. Because it can happend that now other items not valid anymore, I need to check every item again after the list changed. Everything works fine until I delete something and then I got a "cannot change collection during CollectionChanged-Event"-error at the marked point.

My project contains a more complex code (and checks), but for simplicity I try to show some code as example.

Let us say I got a list of numbers from the user and every number needs to have an successor. So I have class that take the Number and the information

public class Number() : INotifyPropertyChanged
{
        public Number(int i)
        {
            digit = i;
            HasSuccessor = null;
        }

        private int digit;
        public int Digit { 
            get { return digit; }
            set { digit = value; OnPropertyChanged(); } }

        private bool? hasSuccessor;
        public bool? HasSuccessor {
            get { return hasSuccessor; }
            set { hasSuccessor = value; OnPropertyChanged(); } }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            if (PropertyChanged == null) return;
            else PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
}

in my Viewmodel I have a ObservableCollection

public class Viewmodel : INotifyPropertyChanged
{
    private ObservableCollection<Lib.Number> numbers = new()
    {
        // Some Debugdata
        new Lib.Number(1),
        new Lib.Number(3),
        new Lib.Number(5),
        new Lib.Number(2),
        new Lib.Number(6)
    };

    public ObservableCollection<Lib.Number> Numbers {
        get { return numbers; }
        set { numbers = value; OnPropertyChanged(); } }

    public Viewmodel()
    {
        Numbers.CollectionChanged += RefreshCheck;
    }

    private void RefreshCheck(object? sender, NotifyCollectionChangedEventArgs e)
    {
        Check();
    }

    public void Check()
    {
        Lib.Model.CheckNumbers(ref numbers);
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        if (PropertyChanged == null) return;
        else PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

in my Model

public static class Model
    {
        public static bool CheckNumbers(ref ObservableCollection<Number> numbers)
        {
            List<Exception> exception = new();

            for (int i = 0; i < numbers.Count; i++)
            {
                Number number = numbers[i];
                Exception? result = CheckNumber(ref number, numbers);
                numbers[i] = number; //line that create the exception

                if(result != null) exception.Add(result);
            }

            if( exception.Any() == true) return false;
            else return true;
        }

        public static Exception? CheckNumber(ref Number number, ObservableCollection<Number> numbers)
        {
            Exception? error = null;

            Number num = number;
            if(numbers != null && numbers.Any() == true)
            {
                if (numbers.Where(item => item.Digit == (num.Digit + 1)).Any() == true)
                    number.HasSuccessor = true;
                else if (numbers.MaxBy(item => item.Digit).Digit == num.Digit)
                    number.HasSuccessor = true;
                else if (numbers.MinBy(item => item.Digit).Digit == num.Digit)
                    number.HasSuccessor = true;
                else
                    number.HasSuccessor = false;
            }
            else
            {
                Exception exception = new ("numbers was null");
            }

            return error;
        }
    }

So, let say the user submit 1,3,5, 2, 6 -> with 4 missing. I will show him errormarks at number 5,6. but he can decide to delete 1,2,3 and import 5,6 because they now valid.

I can force the user to press button after change if items or create a "fire & forget" task that just wait a few milliseconds before starting to recheck, but I think that are not clean solutions... Maybe something like a transaction in a database with stop the change-event till i finish the changes...

Anyone a hint or link for me?

You can't modify the collection during a CollectionChanged event.

You don't need the ref parameters. Number is a reference type and therefore changing it's values outside the collection does also change the object in the collection.
It's a reference type, which means the reference is copied and not the value (memory location). Although the references are not equal, they still point to the same memory location. Thus both references, original and copy, modify the same memory location ie same object instance.

Example

public static void Main()
{
  var person = new Person() { Age = 20 };
  person.Age = 30;
  Console.WriteLine(Person.Age); // Age == 30

  ChangeAgeTo100(person);
  Console.WriteLine(person.Age); // Age == 100

  ChangePersonInstance(person, 50);
  Console.WriteLine(person.Age); // Age == 100

  ChangePersonInstanceRef(ref person, 50);
  Console.WriteLine(person.Age); // Age == 50
}

private void ChangeAgeTo100(Person person)
{
  person.Age = 100;
}

private void ChangePersonInstance(Person person, int newAge)
{
  person = new Person() { Age = newAge };
}

private void ChangePersonInstanceRef(ref Person person, int newAge)
{
  person = new Person() { Age = newAge };
}

Furthermore, you should not pass around Exception objects. Exceptions must be thrown using the throw keyword. In case of argument or null reference exceptions you want the application to crash, so that the developer can fix his code. The exception message will show the exact line of code that has thrown the exception.

The following code is a fixed and improved/simplified version of your critical code section:

// Will throw an exception in case the numbers collection is null or empty
public static void CheckNumbers(ObservableCollection<Number> numbers)
{
  foreach (Number number in numbers)
  {
    CheckNumber(number, numbers);  
  }
}

public static void CheckNumber(Number number, ObservableCollection<Number> numbers)
{
  if (numbers == null)
  {
    throw new ArgumentNullException(nameof(numbers));
  }

  if (!numbers.Any())
  {
    throw new ArgumentException("Collection is empty", nameof(numbers));
  }
        
  number.HasSuccessor = 
    numbers.Any(item => item.Digit == number.Digit + 1)
      || numbers.Max(item => item.Digit) == number.Digit
      || numbers.Min(item => item.Digit) == number.Digit;
}

Also, because you are using the null-conditional operator ?. the explicit null check in OnPropertyChanged is redundant:

protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM