Search code examples
c#xamldata-bindingwinui-3winui

Binding ObservableCollection To a ListView Using MVVM


I having an issue trying to bind an Observable Collection to a ListView control. I am not sure if my design pattern is correct. I am storing a list of Users in the View Model as a ObservableCollection. I am then setting the source of a ListView as that collection. However when I do the ListView.ItemTemplate it doesn't like the following:

<TextBlock Text="{x:Bind Username}" Grid.Column="0"></TextBlock>"

It says the Username was not found in the UserViewModel.

If I change the DataTemplate to use the Model instead the Id/Username works but then the RelayCommand doesn't.

I am using WinUi 3, Entity Framework Core and MVVM Community Toolkit.

This is the code I have so far:

Model

public partial class User: ObservableObject
{
    public int Id { get; set; }

    [ObservableProperty]
    private string _username = string.Empty;
    
    partial void OnUsernameChanged(string? oldValue, string newValue)
    {
        Debug.WriteLine("Username changed");        
    }
}

View Model

public partial class UserViewModel : ObservableObject
{
    RecipeDBContext context;

    private ObservableCollection<User> _users;        
    public ObservableCollection<User> Users
    {
        get { return _users; }
        set
        {
            if (_users != value)
            {
                if (_users != null)
                {
                    _users.CollectionChanged -= OnCollectionChanged;
                }

                _users = value;

                if (_users != null)
                {
                    _users.CollectionChanged += OnCollectionChanged;
                }
            }
        }
    }
    

    public UserViewModel() 
    {
        context = new RecipeDBContext();
        _users = new ObservableCollection<User>();
        UpdateUsers(context.User.ToList());

    }

    public void UpdateUsers(List<User> users)
    {
        _users.Clear();
        foreach (User user in users)
        {
            Users.Add(user);
        }
    }

    void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
        {
            foreach (User newItem in e.NewItems)
            {                                
                context.User.Add(newItem);                
            }
        }

        if (e.OldItems != null)
        {
            foreach (User oldItem in e.OldItems)
            {                
                context.User.Remove(oldItem);                
            }
        }
        context.SaveChanges();
    }

    [RelayCommand]
    public void DeleteUser(object o)
    {
        Debug.WriteLine("Called Delete User");
        if (o != null)
        {
            int id = int.Parse(o.ToString());
            
            using (context)
            {
                try
                {
                    context.User.Remove(new User() { Id = id });
                    context.SaveChanges();
                }
                catch (Exception ex)
                {
                    if (!context.User.Any(i => i.Id == id))
                    {                        
                        return;
                    }
                    else
                    {
                        throw ex;
                    }
                }
            }
        }
    }

    [RelayCommand]
    public void AddUser(object o)
    {
        if (o != null)
        {
            string username=o as string;
            if(username != String.Empty)
            {
                using (context)
                {
                    var user = new User()
                    {                        
                        Username = username,
                    };                    
                    context.User.Add(user);                                       
                    context.SaveChanges();
                }
            }
        }                                
    }

    [RelayCommand]
    private void ModifyName(object o)
    {
        string oldName = "fred";
        if (o != null)
        {
            string newUsername = o as string;
            if (newUsername != String.Empty)
            {
                for (int i = Users.Count - 1; i >= 0; i--)
                {                    
                    if (Users[i].Username == oldName)
                    {
                        Users[i].Username = newUsername;
                        context.SaveChanges();
                    }
                }
            }
            else
            {
                
            }
        }
    }
}

View

<Page
    x:Class="Test.Views.UserPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Test.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:vm="using:Test.ViewModels"    
    mc:Ignorable="d"    
    xmlns:m="using:Test.Models">
    
    <Page.DataContext>
        <vm:UserViewModel x:Name="ViewModel"/>
    </Page.DataContext>

    <Grid>

        <Grid.RowDefinitions>
            <RowDefinition Height="0.2*"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.3*"></ColumnDefinition>
            <ColumnDefinition Width="0.3*"></ColumnDefinition>
            <ColumnDefinition Width="0.3*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Border BorderThickness="1" BorderBrush="Black" />
        <TextBlock FontSize="30" Grid.Row="0" Grid.Column="0">LOGIN</TextBlock>
        <TextBlock Grid.Row="1" Grid.Column="0">User</TextBlock>
        <ListView x:Name="usersListView" Grid.Row="1" Grid.Column="1" ItemsSource="{x:Bind ViewModel.Users, Mode=TwoWay}">
            <ListView.Header>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="0.3*"></ColumnDefinition>
                        <ColumnDefinition Width="0.3*"></ColumnDefinition>
                        <ColumnDefinition Width="0.3*"></ColumnDefinition>
                    </Grid.ColumnDefinitions>
                    <ListViewHeaderItem  Grid.Column="0">
                        <TextBlock>Username</TextBlock>
                    </ListViewHeaderItem>
                    <ListViewHeaderItem  Grid.Column="1">
                        <TextBlock>Recipes</TextBlock>
                    </ListViewHeaderItem>
                    <ListViewHeaderItem  Grid.Column="2">
                        <TextBlock>Delete</TextBlock>
                    </ListViewHeaderItem>
                </Grid>
            </ListView.Header>

            <ListView.ItemTemplate>
                <DataTemplate x:DataType="vm:UserViewModel">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="0.3*"></ColumnDefinition>
                            <ColumnDefinition Width="0.3*"></ColumnDefinition>
                            <ColumnDefinition Width="0.3*"></ColumnDefinition>
                        </Grid.ColumnDefinitions>
                        <TextBlock Text="{x:Bind Username}" Grid.Column="0"></TextBlock>
                        <Button Grid.Column="2" Command="{x:Bind DeleteUserCommand}" CommandParameter="{x:Bind Id}">X</Button>
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Button x:Name="loginUserBtn" Click="loginUserButton_Click" Grid.Row="2" Grid.Column="1">Login</Button>

        <TextBlock Grid.Row="3" Grid.Column="0" FontSize="30">New User</TextBlock>
        <TextBlock Grid.Row="4" Grid.Column="0">Name</TextBlock>
        <TextBox x:Name="newUserName" Grid.Row="4" Grid.Column="1"></TextBox>
        <Button x:Name="addUserBtn" Click="addUserButton_Click" Grid.Row="5" Grid.Column="1">Add User</Button>
        <Button x:Name="addUserBtn1" Command="{x:Bind ViewModel.AddUserCommand}" CommandParameter="{Binding Text, ElementName=newUserName}" Grid.Row="5" Grid.Column="2">Add User1</Button>
        <Button x:Name="getUserBtn" Click="getUserButton_Click" Grid.Row="6" Grid.Column="0">Get All</Button>
    </Grid>
</Page>


Solution

  • Since your ListView is a collection of Users, its ItemTemplate's data type needs to be User.

    Now to bind the delete Button to the ViewModel command, which is outside of the DataTemplate, you need a little trick.

    1. x:Name the Page.
    <Page x:Name="ThisPage" ... />
    
    1. Move the instantiation of UsersViewModel to the code behind.
    <!--
    <Page.DataContext>
        <vm:UsersViewModel x:Name="ViewModel"/>
    </Page.DataContext>
    -->
    
    public sealed partial class UserPage: Page
    {
        public UserPage()
        {
            InitializeComponent();
        }
    
        public UsersViewModel ViewModel { get; } = new();
    }
    
    1. Use Binding with ElementName and Path to bind the command.
    <ListView ItemsSource="{x:Bind ViewModel.Users}">
        <ListView.ItemTemplate>
            <DataTemplate x:DataType="local:User">
                <Grid ColumnDefinitions="*,Auto">
                    <TextBlock
                        Grid.Column="0"
                        Text="{x:Bind Username}" />
                    <Button
                        Grid.Column="1"
                        Command="{Binding ElementName=ThisPage, Path=ViewModel.DeleteUserCommand}"
                        CommandParameter="{x:Bind}"
                        Content="Delete" />
                </Grid>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
    
    public partial class User : ObservableObject
    {
        [ObservableProperty]
        private string _username = string.Empty;
    }
    
    public partial class UsersViewModel : ObservableObject
    {
        [ObservableProperty]
        private ObservableCollection<User> _users = 
            [
                new User { Username = "User1" },
                new User { Username = "User2" },
                new User { Username = "User3" },
            ];
    
        [RelayCommand]
        private void DeleteUser(User user)
        {
            Users.Remove(user);
        }
    }