'Add/Delete Item in real-time from ListView (C# MVVM WPF)

I have a ListView with an ObservableCollection. The class User has a List with Groups. I have another ListView which shows the Groups of the selected User. Now i try to add/delete groups, but they don't refresh in realtime. If i select another user and back to my user, i see the updated list.

XAML:

    <ListView ItemsSource="{Binding ListUsers}" SelectedItem="{Binding SelectedUser}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Name}"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

    [...]

    <ListView ItemsSource="{Binding SelectedUser.Groups}" SelectedItem="{Binding SelectedGroup}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding}"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

Class:

public class User : ObservableObject
{
    private string name;

    public string Name
    {
        get => name;
        set => SetProperty(ref name, value);
    }

    private ObservableCollection<string> groups;
    public ObservableCollection<string> Groups
    {
        get => groups;
        set => SetProperty(ref groups, value);
    }
}

ViewModel:

private ObservableCollection<User> listusers;
public ObservableCollection<User> ListUsers
{
    get => listusers;
    private set => SetProperty(ref listusers, value);
}

private User _selectedUser;
public User SelectedUser
{
    get => _selectedUser;
    set => SetProperty(ref _selectedUser, value);
}

private string _selectedGroup;
public string SelectedGroup
{
    get => _selectedGroup;
    set => SetProperty(ref _selectedGroup, value);
}

public async Task DelGroup()
{
    await Task.Run(() =>
    {
        //SelectedUser.Groups.Remove(SelectedGroup);

        for (int i = 0; i < ListUsers.Count; i++)
        {
            if (SelectedUser.SAMAccountName == ListUsers[i].SAMAccountName)
            {
                ListUsers[i].Groups.Remove(SelectedGroup);
            }
        }
    });
}


Solution 1:[1]

Using a background thread only to move the work to the Dispatcher results in bad performance and should be avoided. At best, the performance remains the same as it would be when deleting the group directly on the UI thread using your code. This means no improvement.

You will get the best performance if you create and maintain a lookup table where the SAMAccountName is the key and the value a collection of User items of that SAMAccountName (in case your table doesn't allow duplicate keys):

public ObservableCollection<User> ListUsers { get; }
private Dictionary<string, IList<User>> UserTable { get; }

public ViewModel()
{
  this.ListUsers = new ObservableCollection<User>();
  this.ListUsers.ColectionChanged += OnUsersChanged;
}

public void DelGroup()
{
  if (!this.UserTable.TryGetValue(this.SelectedUser.SAMAccountName, out IList<User>> usersWithEqualName)
  {
    return;
  }
  foreach (var user in usersWithEqualName)
  {
    user.Group.Remove(this.SelectedGroup);
  }
}

private void OnUsersChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
  switch (e.Action)
  {
    case NotifyCollectionChangedAction.Add:
      foreach (var newUser in e.NewItems.Cast<User>())
      {
        AddUserToLookupTable(newUser);
      }
      break;
    case NotifyCollectionChangedAction.Remove:
    case NotifyCollectionChangedAction.Replace:
      foreach (var oldUser in e.OldItems.Cast<User>())
      {
        RemoveUserFromLookupTable(oldUser);
      }
      break;
    case NotifyCollectionChangedAction.Reset:
      this.UserTable.Clear();
      break;
  }
}

private void AddUserToLookupTable(User user)
{
  if (!this.UserTable.TryGetValue(user.SAMAccountName, out IList<User>> usersWithEqualName)
  {
    usersWithEqualName = new List<User>();
    this.UserTable.Add(user.SAMAccountName, usersWithEqualName);
  }
  usersWithEqualName.Add(user);
}

private void RemoveUserFromLookupTable(User user)
{
  if (!this.UserTable.TryGetValue(user.SAMAccountName, out IList<User>> usersWithEqualName)
  {
    return;
  }
  usersWithEqualName.Remove(user);
  if (!usersWithEqualName.Any())
  {
    this.UserTable.Remove(user.SAMAccountName);
  }
}

A more simple solution would be to ensure that User.Group is the same instance for all User instances. This way, removing a group from one User.Group collection, will remove it from all. This would eliminate the above solution and even perform better. But this solution is only feasible if all User instances have the same groups.

Solution 2:[2]

You need to invoke the action through the dispatcher because you start it from a ThreadPool thread - using Task.Run.
You could drop the Dispatcher invokation and still benefit from a non blocking asyc call, if you rewrite, so that the modifying UIElement access happens on the Main thread.
For example like:

public async Task DelGroup()
{
    foreach (int index in await Task.Run(GetRemoveIndices).ConfigureAwait(true)) {
        ListUsers[index].Groups.Remove(SelectedGroup);
    };

    IEnumerable<int> GetRemoveIndices() {
        for (int i = ListUsers.Count-1; i >= 0; i--) {
            if (SelectedUser.SAMAccountName == ListUsers[i].SAMAccountName) {
                 yield return i;
            }
        }
    }    
}

Solution 3:[3]

Found the answer:

Application.Current.Dispatcher.Invoke(() => SelectedUser.Groups.Remove(SelectedGroup));

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 lidqy
Solution 3 Bill Tür stands with Ukraine