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>
Since your ListView
is a collection of User
s, 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.
x:Name
the Page
.<Page x:Name="ThisPage" ... />
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();
}
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);
}
}