WPF MVVM: How to reflect changes of ObservableCollection in the UI - c#

I am relatively new in WPF and am trying to understand the MVVM pattern and how data-binding works with ObservableCollection, in order to build the application I am working on with MVVM. I have created a sample of my application that has a MainWindow where, depending on which button the user presses, a different View (UserControl) is displayed. The general idea is that the user will have access to the data of some elements from a database (e.g.: Customers, Products, etc.) and will be able to add new and edit, or delete, existing ones.
So, there is a CustomerView, with its CustomerViewModel, and a ProductView, with its ProductViewModel respectively. Also, there are two classes (Customer.cs & Product.cs) that represent the Models. The structure of the project is displayed here.
The MainWindow.xaml is as follows:
<Window.Resources>
<DataTemplate DataType="{x:Type viewModels:CustomerViewModel}">
<views:CustomerView DataContext="{Binding}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:ProductViewModel}">
<views:ProductView DataContext="{Binding}"/>
</DataTemplate>
</Window.Resources>
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20*"/>
<ColumnDefinition Width="80*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" HorizontalAlignment="Center" VerticalAlignment="Center">
<Button x:Name="btnCustomers" Click="btnCustomers_Click" Content="Customers" Width="80" Height="50" Margin="10"/>
<Button x:Name="btnProducts" Click="btnProducts_Click" Content="Products" Width="80" Height="50" Margin="10"/>
</StackPanel>
<Grid Grid.Column="1">
<ContentControl Grid.Column="0" Content="{Binding}"/>
</Grid>
</Grid>
and the code behind MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public CustomerViewModel customerVM;
public ProductViewModel productVM;
public MainWindow()
{
InitializeComponent();
}
private void btnCustomers_Click(object sender, RoutedEventArgs e)
{
if (customerVM == null)
{
customerVM = new CustomerViewModel();
}
this.DataContext = customerVM;
}
private void btnProducts_Click(object sender, RoutedEventArgs e)
{
if (productVM == null)
{
productVM = new ProductViewModel();
}
this.DataContext = productVM;
}
}
Finally, the CustomerView.xaml is as follows:
<UserControl.Resources>
<viewModel:CustomerViewModel x:Key="customerVM"/>
<!-- Styling code here...-->
</UserControl.Resources>
<Grid DataContext="{StaticResource ResourceKey=customerVM}">
<Grid.RowDefinitions>
<RowDefinition Height="2*"/>
<RowDefinition Height="7*"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBlock Text="Customers" FontSize="18"/>
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5*"/>
<ColumnDefinition Width="5*"/>
</Grid.ColumnDefinitions>
<ComboBox x:Name="cmbCustomers" Grid.Column="0" VerticalAlignment="Top"
IsEditable="True"
Text="Select customer"
ItemsSource="{Binding}"
DisplayMemberPath="FullName" IsSynchronizedWithCurrentItem="True">
</ComboBox>
<StackPanel Grid.Column="1" Margin="5">
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Column="0" Text="Id:" />
<TextBlock Grid.Column="1" x:Name="txtId" Text="{Binding Path=Id}" FontSize="16"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Column="0" Text="Name:" />
<TextBlock Grid.Column="1" x:Name="txtFirstName" Text="{Binding Path=FirstName}" FontSize="16"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Column="0" Text="Surname:" />
<TextBlock Grid.Column="1" x:Name="txtLastName" Text="{Binding Path=LastName}" FontSize="16"/>
</StackPanel>
</StackPanel>
</Grid>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="btnAddNew" Content="Add New" Click="btnAddNew_Click"/>
<Button x:Name="btnDelete" Content="Delete Customer" Click="btnDelete_Click"/>
</StackPanel>
</Grid>
and the CustomerViewModel.cs:
public class CustomerViewModel : ObservableCollection<Customer>
{
public CustomerViewModel()
{
LoadCustomers();
}
private void LoadCustomers()
{
for (int i = 1; i <= 5; i++)
{
var customer = new Customer()
{
Id = i,
FirstName = "Customer_" + i.ToString(),
LastName = "Surname_" + i.ToString()
};
this.Add(customer);
}
}
public void AddNewCustomer(int id)
{
var customer = new Customer()
{
Id = id,
FirstName = "Customer_" + id.ToString(),
LastName = "Surname_" + id.ToString()
};
Add(customer);
}
}
Please note that the ProductView.xaml & ProductViewModel.cs are similar.
Currently, when the user presses the "Customers" or the "Products" button of the
MainWindow, then the respective View is displayed and the collections are loaded
according to the LoadCustomers (or LoadProducts) method, which is called by the
ViewModel's constructor. Also, when the user selects a different object from the
ComboBox, then its properties are displayed correctly (i.e. Id, Name, etc.). The
problem is when the user adds a new (or deletes an existing) element.
Question 1: Which is the correct and best way to update a changed Observable
Collection of an element and reflect its changes in the UI (Combobox, properties, etc.)?
Question 2: During testing this project I noticed that the constructor of the
ViewModels (consequently the LoadCustomers & LoadProducts method) are called twice. However, it is only called when the user presses the Customers or the
Products button respectively. Is it also called via the XAML data binding? Is
this the optimum implementation?

Your first question is basically a UX one, there is no correct or "best" way. You'll definitely end up using some sort of ItemsControl, but which one depends heavily on how you want your users to interact with it.
To your second question, you have a few mistakes in your code:
<viewModel:CustomerViewModel x:Key="customerVM"/> Instantiates a new view model, apart from the one that the main application created
Grid DataContext="{StaticResource ResourceKey=customerVM}" Then uses this "local" view model, ignoring the inherited one from the main application
That's why you see the constructor fire twice, you are constructing two instances! Eliminate the local VM and don't assign the DC on the grid. Other issues:
<views:ProductView DataContext="{Binding}"/> The DataContext assignment is total unnecessary, by virtue of being in the data template it's data context is already set up
<ContentControl Grid.Column="0" Content="{Binding}"/> Yuck, you should have a "MainViewModel" with a property that this uses. Don't make it be the whole data context
Lack of commands for your button clicks (related to the bullet above)

There is 3 kinds of Change Notification you need with Lists in MVVM:
Change Notificataions on every property of the list items.
Change Notification on the property exposing the list, in case the whole instance has to be replaced (wich is common because of 3)
Change Notification if elements are added to or removed from the collection. That is the only thing ObservableCollection takes care off. Unfortunately there is no Addrange option, so bulk operations wil lsmwap the GUI with Notifications. That is what Nr. 2 is there for.
As advanced option, consider exposing the CollectionView rather then the raw Collection. WPF GUI elements do not bind to raw Collections, only CollectionViews. But if you do not hand them one, they will create one themself.

Related

WPF Enable Highlight, Copy, and Paste inside a Listbox

I have a list of strings that I want to display on a menu. I used a Listbox and it works just that it won't let me highlight or copy/paste.
Here is my XAML
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="500"/>
<ColumnDefinition Width="500"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="450"/>
<RowDefinition Height="318"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="1" Grid.Column="1" x:Name="uiOCRData" />
</Grid>
Heres what I have in C#
List<string> lines = new List<string>();
uiOCRData.ItemsSource = lines;
Thanks for the help!
You must use a ListBox.ItemTemplate so that you can include a control inside your ListBox.
Since you want to be able to select text etc., the best option is to use a TextBox.
<ListBox Grid.Row="0" Name="uiOCRData">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=.}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
EDIT
Let's say you want to bind to a list of some class objects instead of a simple list of strings. Say your class looks like this:
public class Data
{
public int Id { get; set; }
public string Name { get; set; }
}
Then you can bind to any one of chosen Properties of the class like this:
<ListBox Grid.Row="0" Name="uiOCRData">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Width="100" Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Not able to bind to listview in Windows 10 apps (UWP)

I have created sample app for demonstrating the issue.
Sorry its quite difficult to put all the code here since there are model classes, datamodel, service file which fetches the data from rest api.
So only few files are being included which gives information.
_placeList = await DataModel.PlaceDataSource.GetData(url); this line of statement from PlacePage.xaml.cs file is actually fetching records but doesn't get binded and displayed in listview.
But gridViewPlaces.ItemsSource = await DataModel.PlaceDataSource.GetData(url); works.
You can find the source code here. Project Download Link
MainPage.xaml
<SplitView x:Name="splitView" IsPaneOpen="True" OpenPaneLength="250" Grid.Row="1" DisplayMode="Inline">
<SplitView.Pane>
...
</SplitView.Pane>
<SplitView.Content>
<Grid>
<Frame x:Name="rootFrame" />
</Grid>
</SplitView.Content>
</SplitView>
PlacePage.xaml
<GridView Name="gridViewPlaces" ItemsSource="{x:Bind PlaceList}" SelectionMode="Single">
<GridView.ItemTemplate>
<DataTemplate>
<Grid Width="200" Height="Auto">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Key" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Name}" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="Value" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Value}" />
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
PagePage.xaml.cs file
private IEnumerable<Place> _placeList;
public IEnumerable<Place> PlaceList
{
get { return _placeList; }
}
public event EventHandler GroupsLoaded;
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
url = e.Parameter.ToString();
LoadPlaces();
}
async private void LoadPlaces()
{
_placeList = await DataModel.PlaceDataSource.GetData(url);
//gridViewPlaces.ItemsSource = await DataModel.PlaceDataSource.GetData(url); // This works
gridViewPlaces.UpdateLayout();
if (GroupsLoaded != null)
GroupsLoaded(this, new EventArgs());
}
Your PlaceList property needs to fire notifications to let the binding know there’s a change. As is, when you replace _placeList you don't notify anybody that PlaceList changed and so nothing updates. The typical pattern here is to initialize the PlaceList property read only and then add things to that existing collection rather than swapping out the collection, though if you notify that you've swapped the collection that should work too.
Additionally, the IEnumerable inside PlaceList needs to provide notifications when its contents change. The standard way to do this is to make it an ObservableCollection since OC implements INotifyPropertyChanged and INotifyCollectionChanged for you. See theBinding to collections Quickstart

Preloading ListView in WPF

I am creating a little application that has one feature to show list of people in popup when user click on button. That list, that is in popup, can be pretty big, it can have as many as 3k entries. I am using virtualization, and I don't have problem with performance when list is drawn. But, when user open application for the first time and click on button for popup with list of people, it can take 2-5 seconds before popup is shown. After, that if user try to open popup again popup will be open without delay.
So my question is could I say to ListView to prepare items while it is not shown. Because there is very big chance that user will use application for quiet some time before it will need this popup.
Can I optimize this in some other way? Is there some collection that is better for this purpose in WPF?
Also people collection will be populated when page is loaded, and popup is at beginning closed.
Here it is code that I have:
public class AddressBookViewModel : BaseViewModel
{
...
private ObservableCollection<PeopleModel> people;
...
public ObservableCollection<PeopleModel> People
{
get { return people; }
}
}
public class PeopleModel
{
public PeopleModel(string address, string name)
{
Address = address;
Name = name;
}
public string Name{ get; set; }
public string Address { get; set; }
}
<Button x:Name="btnChoosePerson"
Command="{Binding TogglePeopleAddressPopupCommand}"
Content="..." />
<Popup MaxHeight="520"
IsOpen="{Binding ShowPeopleAddressPopup}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=btnChoosePerson}"
StaysOpen="False">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="5" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListView
Grid.Row="2"
Width="500"
HorizontalAlignment="Left"
ItemsSource="{Binding People}"
SelectionMode="Single"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.CacheLength="10"
VirtualizingPanel.CacheLengthUnit="Item">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
HorizontalAlignment="Left"
Text="{Binding Address}" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Right"
Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Popup>

WPF Adding an object to ListBox with existing ItemsSource

Hey I have a list box that I set the ItemsSource to be an ObservableCollection of objects from my database, and I need to add a single object at the end of this list. However I keep getting an invalid operation exception. Somehow my listbox is in use (which in my mind is a given as it is displayed and already have items inside.) Here is my code for the list box:
<ListBox x:Name="CarList" SelectionChanged="ItemSelected" ScrollViewer.HorizontalScrollBarVisibility="Disabled" Background="{x:Null}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel FlowDirection="LeftToRight" ItemHeight="300" ItemWidth="300"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="10,10">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="35" />
</Grid.RowDefinitions>
<Image Grid.Row="0" Source="{Binding image_path}" VerticalAlignment="Stretch"/>
<Grid Grid.Row="1" Background="SteelBlue">
<TextBlock HorizontalAlignment="Left" VerticalAlignment="Center" Margin="3" Text="{Binding model}"/>
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Center" Margin="3" Text="{Binding price}"/>
</Grid>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And I first set the ItemsSource like so:
CarList.ItemsSource = CarController.GetAllCars();
And then want to add my custom object like this:
ListBoxItem carAdd = new ListBoxItem();
carAdd.Content = new CarModel{ image_path = "/../Assets/add-512.png", id=-1};
CarList.Items.Add(carAdd);
But alas the last operation fails with this message:
Operation is not valid while ItemsSource is in use. Access and modify
elements with ItemsControl.ItemsSource instead.
I have looked for a few other suggestions but all use strings and single bindings in their examples and thus I haven't been able to figure out what exactly to do. If anyone got a suggestion it would be much appreciated.
-
Thanks.
You need to add the item to the items source, and the source should be observable so that the ListBox takes the new item into account:
var cars = new ObservableCollection<CarModel>(CarController.GetAllCars());
CarList.ItemsSource = cars;
...
var car = new CarModel{ image_path = "/../Assets/add-512.png", id=-1};
cars.Add(car);

Custom ListView from inside HubAppSection -- Windows Phone 8.1

I am currently building a Windows Phone Applicaation, based off of the HubAppTemplate.
The template comes with a sample .JSON data source that it uses to populate the data of each HubSection. However, I want to use a non JSON type of data as the basis of my code. Inside my C# code, I need to make a function call to my backend to get the type of data I want out of it.
I can put this data inside of my own custom list (on the C# side), but how can I make that list act as the data source for my HubSection? Any old listview/list box works perfectly. Basically, I need help wiring the C# to the XAML -- the main issue is that I cannot access my listView inside of the datatemplate by name.
Can anyone give me some pointers to get going in the right direction?
Here is some reference code to show you what I am talking about:
<HubSection x:Uid="Clubs" Header="Clubs" DataContext="{Binding Groups}" HeaderTemplate="{ThemeResource HubSectionHeaderTemplate}">
<DataTemplate>
<ListView Name="ClubsList"
IsItemClickEnabled="True"
ItemsSource="{Binding}"
ItemClick="GroupSection_ItemClick"
ContinuumNavigationTransitionInfo.ExitElementContainer="True">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,27.5">
<TextBlock Text="{Binding Title}" Style="{ThemeResource ListViewItemTextBlockStyle}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DataTemplate>
</HubSection>
The above XAML is basically pulled straight from the hubapp template. I want to be able to use my own itemssource inside of that ListView that is generated from my C# code -- however, I cannot figure out how this ItemsSource works. I also cannot access my listview by name (ClubsList).
Here is the initialization code going on up top (wasn't sure if it was important to post this or not):
<Page
x:Class="HubAppTemplate.HubPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:HubAppTemplate"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:data="using:HubAppTemplate.Data"
DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
d:DataContext="{Binding Source={d:DesignData Source=/DataModel/SampleData.json, Type=data:SampleDataSource}}"
mc:Ignorable="d">
<Page.Resources>
<DataTemplate x:Key="HubSectionHeaderTemplate">
<TextBlock Margin="0,0,0,-9.5" Text="{Binding}"/>
</DataTemplate>
<!-- Grid-appropriate item template as seen in section 2 -->
<DataTemplate x:Key="Standard200x180TileItemTemplate">
<Grid Margin="0,0,9.5,9.5" Background="{ThemeResource ListViewItemPlaceholderBackgroundThemeBrush}">
<Image Source="{Binding ImagePath}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}" Height="138.5" Width="138.5"/>
<TextBlock Text="{Binding Title}" VerticalAlignment="Bottom" Margin="9.5,0,0,6.5" Style="{ThemeResource BaseTextBlockStyle}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="StandardTripleLineItemTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Background="{ThemeResource ListViewItemPlaceholderBackgroundThemeBrush}" Margin="0,9.5,0,0" Grid.Column="0" HorizontalAlignment="Left">
<Image Source="{Binding ImagePath}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}" Height="79" Width="79"/>
</Border>
<StackPanel Grid.Column="1" Margin="14.5,0,0,0">
<TextBlock Text="{Binding Title}" Style="{ThemeResource ListViewItemTextBlockStyle}"/>
<TextBlock Text="{Binding Description}" Style="{ThemeResource ListViewItemContentTextBlockStyle}" Foreground="{ThemeResource PhoneMidBrush}" />
<TextBlock Text="{Binding Subtitle}" Style="{ThemeResource ListViewItemSubheaderTextBlockStyle}" />
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="StandardDoubleLineItemTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Background="{ThemeResource ListViewItemPlaceholderBackgroundThemeBrush}" Margin="0,9.5,0,0" Grid.Column="0" HorizontalAlignment="Left">
<Image Source="{Binding ImagePath}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}" Height="79" Width="79"/>
</Border>
<StackPanel Grid.Column="1" Margin="14.5,0,0,0">
<TextBlock Text="{Binding Title}" Style="{ThemeResource ListViewItemTextBlockStyle}"/>
<TextBlock Text="{Binding Subtitle}" Style="{ThemeResource ListViewItemSubheaderTextBlockStyle}"/>
</StackPanel>
</Grid>
</DataTemplate>
</Page.Resources>
<Grid x:Name="LayoutRoot">
<Hub x:Name="Hub" x:Uid="Hub" Header="Club Alert" Background="{ThemeResource HubBackgroundImageBrush}">
It is pulling from the JSON backend, but I want to just use my own custom listview for each section. Deleting the DataSource and data template headers gives me errors, however.
Thank you so much for your help in advance!
--A total newbie
HubSection elements require their contents to be populated via a template, so you can't just remove the <DataTemplate> tags, unfortunately. However, there is a simple way to accomplish what you are trying to do, if I understand you correctly.
If you're starting with the default Hub template, you should have this function in your HubPage.xaml.cs file
private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
// TODO: Create an appropriate data model for your problem domain to replace the sample data
var sampleDataGroups = await SampleDataSource.GetGroupsAsync();
this.DefaultViewModel["Groups"] = sampleDataGroups;
MainViewModel viewModel = DataContext as MainViewModel;
if (!viewModel.IsDataLoaded)
{
viewModel.Load();
}
}
this.DefaultViewModel is just a Dictionary, and they have loaded the sample JSON into a variable and stored this in the ["Groups"] key of the dictionary. Since the Page's DataContext is being bound to {Binding DefaultViewModel, RelativeSource={RelativeSource Self}}, the HubSection's DataContext is being bound to {Binding Groups}, and the ItemsSource of the ListView in each DataTemplate is being bound to {Binding}, each element of the loaded JSON is being used to fill the items of the ListView.
A simple solution would be to assign this.DefaultViewModel["Groups"] to the C# List you are creating from the data you load from your back end.
Something like this:
private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
// TODO: Create an appropriate data model for your problem domain to replace the sample data
var myData = await GetListOfThingsFromBackend();
this.DefaultViewModel["Groups"] = myData;
MainViewModel viewModel = DataContext as MainViewModel;
if (!viewModel.IsDataLoaded)
{
viewModel.Load();
}
}
A better approach would probably be to separate out all ViewModel functionality to it's own class that is better suited to your needs, and then adjust the various DataContext properties throughout the XAML, but that would likely take more time. I can elaborate if needed, but the simple solution is probably enough for now.

Categories

Resources