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.
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>
I'm trying to create tool tip for ListView rows.
Current data structure used to generate listview:
public class Map
{
public string Name{get;set;}
public List<Difficulty> Difficulties{get;set;}
}
public class Difficulty
{
public int ID{get;set;}
public string Name{get;set;}
}
// Items to list are added like.
ListView.Items.Add(new Map(){....});
This is what I have with tool tip XAML code:
<Setter Property="ToolTip">
<Setter.Value>
<ItemsControl ItemsSource="{Binding Difficulties}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Name}" />
<TextBlock Grid.Column="1" Text="{Binding ID}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Setter.Value>
</Setter>
It all works, but what I need is to display Name from Map, not Difficulty.
Try
Text="{Binding DataContext.Name, RelativeSource={RelativeSource AncestorType=ListViewItem}}"
The problem with this is probably obvious but im struggling to see it.
I have the following XAML:
<ItemsControl x:Name="contentList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" TextWrapping="Wrap" />
<ItemsControl x:Name="imageContent" Grid.Column="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ImageCollection.FullName}" TextWrapping="Wrap" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I've set up the itemsSource for contentList like so:
contentList.ItemsSource = myObservableCollection;
However when I try to do the same for imageContent, I can't seem to access it via IntelliSense.
I've tried a clean/rebuild of the Project and it's made no difference.
Do I need to access imageContent a different way?
I want to use myObservableCollection for both contentList and imageContent as it has the following structure:
Name (String)
ImageCollection (ObservableCollection)
With an aim to produce the following UI:
You need to define another ObservableCollection within the list objects of your outer collection. Something like this:
ObservableCollection<MyObject> OuterList = new ObservableCollection<MyObject>();
//...
public class MyObject
{
public ObservableCollection<FileInfo> ImageCollection {get; set;}
public MyObject()
{
ImageCollection = new ObservableCollection<FileInfo>();
}
}
Then just update your xaml like so:
...
<ItemsControl x:Name="imageContent" ItemsSource="{Binding ImageCollection}">
...
So this will cause each item in your outer list to hold it's own observable collection holding it's list.
Also with this change make sure you update the binding on your text block, since each item will represent a FileInfo object you can simply write this:
<DataTemplate>
<TextBlock Text="{Binding FullName}" TextWrapping="Wrap" />
</DataTemplate>
I'm trying to make a budget program. Where I need to have groupboxes with a list of textblocks inside.
<ItemsControl DataContext="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<GroupBox Header="{Binding}">
<ItemsControl DataContext="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Text}" />
<TextBlock Text="{Binding Value}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I need somehow to databind a list (perhaps?) with groupboxes so I'd create a list of group boxes, with some lines inside that would be a text with a currency value. So that I could create a group called "Apartment", with two lines "Rent $3000" and "Maintenance $150". Then I could have a second group called "Car" with lines "Insurance", "Loan" and "Maintenance" for instance.
But how would I databind this? And how would I need in C# to perform this. I'm at a loss.
Building off of Jay's comment, you would want to create a Hierarchical data model. Note I have left implementing INotifyPropertyChanged on the properties to you
public class BudgetLineItem : INotifyPropertyChanged
{
public string Name { get; set; }
public decimal Cost { get; set; }
}
public class BudgetGroup : INotifyPropertyChanged
{
public string GroupName { get; set; }
public ObservableCollection<BudgetLineItem> LineItems { get; set; }
}
public class BudgetViewModel : INotifyPropertyChanged
{
public ObservableCollection<BudgetGroup> BudgetGroups { get; set; }
}
Then your data-template would look like this:
<ItemsControl DataContext="{Binding ViewModel}"
ItemsSource="{Binding BudgetGroups}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<GroupBox Header="{Binding GroupName}">
<ItemsControl ItemsSource="{Binding LineItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Cost}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I could be off base here, but it sounds like you want to change the DataTemplate based on the type of object that is being bound from a list of heterogeneous objects.
If that's the case, you want to look into DataTemplateSelectors or create DataTemplates for each of the types you want to support in the list.
For example, for an Apartment you might have:
<DataTemplate DataType="local:ApartmentBudget">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Text}" />
<TextBlock Text="{Binding Value}" />
</StackPanel>
</DataTemplate>
a Car may look like:
<DataTemplate DataType="local:CarBudget">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Insurance}" />
<TextBlock Text="{Binding Loan}" />
<TextBlock Text="{Binding Maintenance}" />
</StackPanel>
</DataTemplate>
Then your ItemsControl can be set like:
<ItemsControl ItemSource="{Binding BudgetItems}">
The correct DataTemplate will be picked based on the data type. You can have even more control by creating a custom DataTemplateSelector.
See https://msdn.microsoft.com/en-us/library/ms742521(v=vs.100).aspx for more information.