I am new to WPF and wondering, how I could bind multiple and different Controls to a ItemControl in a C# WPF Application?
With my current code I can just put labels and textboxes into my ItemControl. Because of the template definition in my XAML.
But I like to be flexible to put labels and other Controls like combobox, datepicker, etc. into my ItemControl...
How could I do this?
I attached my current XAML Code with a Data Template and the code behind. And here a small picture of the current look and my target what I want:
XAML-CODE:
<Window x:Class="WPFFormTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="400">
<Grid>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="icFields">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="FieldTypes" Margin="10,10,10,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Content="{Binding FieldName}" />
<TextBox Grid.Column="1" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
C#-Code:
namespace WPFFormTest
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<Fields> fields = new List<Fields>();
fields.Add(new Fields() { FieldName = "test1", FieldValue = "1" });
fields.Add(new Fields() { FieldName = "test2", FieldValue = "2" });
fields.Add(new Fields() { FieldName = "test3", FieldValue = "3" });
icFields.ItemsSource = fields;
}
}
public class Fields
{
public string FieldName { get; set; }
public string FieldValue { get; set; }
public string FieldType { get; set; }
}
}
One Approach could be: add all needed controls to the template and switch the visibility. But some help from the view model is needed:
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ScrollViewer.Resources>
<BooleanToVisibilityConverter x:Key="BoolConverter"/>
</ScrollViewer.Resources>
<ItemsControl x:Name="icFields">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="FieldTypes" Margin="10,10,10,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Content="{Binding FieldName}" />
<TextBox Grid.Column="1" Visibility="{Binding ShowText, Converter={StaticResource BoolConverter}}"/>
<CheckBox Grid.Column="1" Visibility="{Binding ShowCheckBox, Converter={StaticResource BoolConverter}}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
public class Fields
{
public string FieldName
{
get;
set;
}
public string FieldValue
{
get;
set;
}
public bool ShowText
{
get;
set;
}
public bool ShowCheckBox
{
get;
set;
}
private int fieldType;
public int FieldType
{
get
{
return fieldType;
}
set
{
fieldType = value;
ShowText = false;
ShowCheckBox = false;
switch (fieldType)
{
case 0:
ShowText = true;
break;
case 1:
ShowCheckBox = true;
break;
}
}
}
}
You might want to consider using an MVVM framework. I know that Caliburn.Micro has some built-in features to make this sort of thing easy, using actual OO features like polymorphism.
First you would define a separate viewmodel class for each item type:
public class FieldViewModel { }
public class TextViewModel : FieldViewModel { public string Value {get;set;} }
public class DateViewModel : FieldViewModel { public DateTime Value {get;set;} }
Then you can define a UserControl called TextView and another called DateView. When you bind your list of FieldViewModel to the UI, Caliburn.Micro will automatically create the right type of view and bind its properties.
It takes a little bit of work to get Caliburn.Micro up and running (you need to hook up the bootstrapper and create a shell viewmodel, at the very least) so I won't provide a complete code sample here -- I'm just trying to give you a flavor for whether it would solve your problem. For the actual code you need, work your way through their documentation; they do a good job of walking you through everything.
The simplest way to do that is to just provide different DataTemplates for different data types. For example, if you had a Person class, you could define a DataTemplate with Name, Age, and Sex TextBox fields, whereas if you had a Car class, you could define a DataTemplate with Make, Model and Year ComboBox fields:
<DataTemplate DataType="{x:Type DataTypes:Person}">
<!--Define TextBoxes here-->
</DataTemplate>
<DataTemplate DataType="{x:Type DataTypes:Car}">
<!--Define ComboBoxes here-->
</DataTemplate>
By defining these DataTemplates without specifying the x:Key reference values, this means that they will be automatically applied by the Framework, whenever it comes across any instances of these types. Therefore, this will display Person details in TextBoxes:
<ItemsControl ItemsSource="{Binding Persons}" ... />
And this will display Car object details in ComboBoxes:
<ItemsControl ItemsSource="{Binding Cars}" ... />
Related
I've got a TabControl that is bound to its viewmodel Items property:
<TabControl ItemsSource="{Binding Items}"/>
This is the viewmodel class:
public class ViewModel
{
public ObservableCollection<object> Items { get; } = new ObservableCollection<object>();
public ViewModel()
{
Items.Add(new Person() { FirstName = "Alan", LastName = "Turing" });
Items.Add(new Car() { ModelName = "Fiesta", Manifacturer = "Ford" });
}
}
Items's type is ObservableCollection<object>; this is because each tab represents a different kind of object, in this example a Person and a Car.
Question
I want to bind the tab's header to a text that is not available as a property of the bound classes, let say a manually provided header.
For example, I would like the first tab to have the text "Person" as header and the second to have the text "Car" as header:
I thought of:
adding the Header property to each class. It's easy and straightforward from the binding point of view, but this is sort of a generalized viewer and not all the interfaces of the input data can be modified.
an "attached property" would still require to act at class-level; yet feasible but probably a bit overkill
A Dictionary<object, string> defined at the viewmodel level, let's call it Headers.
Each time i add an object to the collection, i would also add an Header to the Headers dictionary. Its key would be the object itself.
Person p = new Person() { FirstName = "Alan", LastName = "Turing" };
Headers.Add(p, "Person");
Items.Add(p);
But how to write the binding expression then?
<TabControl ItemsSource="{Binding Items}">
<TabControl.ItemTemplate>
<DataTemplate>
<Label Content="{Binding ????????}"/>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
Even if possible, this solution seems to be not very 'binding-friendly'.
There seems to be two problems here: dynamically attach an information to an instance, and access it via binding.
Any ideas?
Postscript - the code
The code, for completeness.
My classes:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Car
{
public string ModelName { get; set; }
public string Manifacturer { get; set; }
}
My DataTemplate, defined in the <Winodow.Resources> tag.
<DataTemplate DataType="{x:Type local:Person}">
<StackPanel>
<Label Content="{Binding FirstName}"/>
<Label Content="{Binding LastName}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Car}">
<StackPanel>
<Label Content="{Binding Manifacturer}"/>
<Label Content="{Binding ModelName}"/>
</StackPanel>
</DataTemplate>
You can also override the ToString() method for your model classed and return your friendly name from there.
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public override string ToString()
{
return "Person";
}
}
Template
<TabControl ItemsSource="{Binding Items}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
I'd like to use a nested ObservableCollection in MVVM in order to add as many groups and users as possible. However, I don't know how to create/add a new user. I also don't know how to bind the new users to the XAML.
(NOTE: this time, I just need two persons.)
I'm new to C#, WPF and MVVM.
I'm learning MVVM referring to this site: https://riptutorial.com/wpf/example/992/basic-mvvm-example-using-wpf-and-csharp
I've been trying this since last week, but no luck.
I tried:
outerObservableCollection.Add(
new ObservableCollection<User>
{
{
FirstName = "Jane",
LastName = "June",
BirthDate = DateTime.Now.AddYears(-20)
}
}
);
... which ends up with the following error:
The name 'BirthDate' does not exist in the current context
(I guess the cause of this issue is that I didn't create a 'user' object, so 'user.BirthDate' is not accessible.)
Let me show the entire codes.
MainWindow.xaml:
<Window x:Class="MVVM_RIP_Tutorial.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MVVM_RIP_Tutorial"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="32.8"/>
<RowDefinition Height="28.8"/>
<RowDefinition Height="38*"/>
<RowDefinition Height="37*"/>
<RowDefinition Height="38*"/>
<RowDefinition Height="155*"/>
</Grid.RowDefinitions>
<!--1st Person-->
<TextBlock Grid.Column="1" Grid.Row="0" Margin="320.6,4,398.6,3.2" Text="{Binding FullName}" HorizontalAlignment="Center" FontWeight="Bold" Width="0"/>
<Label Grid.Column="0" Grid.Row="1" Margin="0,4.8,4.4,2.8" Content="First Name:" HorizontalAlignment="Right" Width="70"/>
<!-- UpdateSourceTrigger=PropertyChanged makes sure that changes in the TextBoxes are immediately applied to the model. -->
<TextBox Grid.Column="1" Grid.Row="1" Margin="3.6,4.8,0,1.8" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="2" Margin="0,5.2,4.4,1.6" Content="Last Name:" HorizontalAlignment="Right" Width="69"/>
<TextBox Grid.Column="1" Grid.Row="2" Margin="3.6,5.2,0,1.6" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="3" Margin="0,6.4,4.4,4.2" Content="Age:" HorizontalAlignment="Right" Grid.RowSpan="2" Width="33"/>
<TextBlock Grid.Column="1" Grid.Row="3" Margin="3.6,6.4,0,10.2" Text="{Binding Age}" HorizontalAlignment="Left" Grid.RowSpan="2" Width="0"/>
<!--2nd Person-->
<!--<TextBlock Grid.Column="1" Grid.Row="4" Margin="320.6,4,398.6,3.2" Text="{Binding FullName}" HorizontalAlignment="Center" FontWeight="Bold" Width="0"/>
<Label Grid.Column="0" Grid.Row="5" Margin="0,4.8,4.4,2.8" Content="First Name:" HorizontalAlignment="Right" Width="70"/>
--><!-- UpdateSourceTrigger=PropertyChanged makes sure that changes in the TextBoxes are immediately applied to the model. --><!--
<TextBox Grid.Column="1" Grid.Row="5" Margin="3.6,4.8,0,1.8" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="6" Margin="0,5.2,4.4,1.6" Content="Last Name:" HorizontalAlignment="Right" Width="69"/>
<TextBox Grid.Column="1" Grid.Row="6" Margin="3.6,5.2,0,1.6" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="7" Margin="0,6.4,4.4,4.2" Content="Age:" HorizontalAlignment="Right" Grid.RowSpan="2" Width="33"/>
<TextBlock Grid.Column="1" Grid.Row="7" Margin="3.6,6.4,0,10.2" Text="{Binding Age}" HorizontalAlignment="Left" Grid.RowSpan="2" Width="0"/>-->
</Grid>
</Window>
MainWindow.xaml.cs:
using System.Windows;
namespace MVVM_RIP_Tutorial
{
public partial class MainWindow : Window
{
private readonly MyViewModel _viewModel;
public MainWindow()
{
InitializeComponent();
_viewModel = new MyViewModel();
// The DataContext serves as the starting point of Binding Paths
DataContext = _viewModel;
}
}
}
User.cs:
using System;
namespace MVVM_RIP_Tutorial
{
sealed class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
}
MyViewModel.cs:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace MVVM_RIP_Tutorial
{
// INotifyPropertyChanged notifies the View of property changes, so that Bindings are updated.
sealed class MyViewModel : INotifyPropertyChanged
{
private User user;
ObservableCollection<ObservableCollection<User>> outerObservableCollection
= new ObservableCollection<ObservableCollection<User>>();
//ObservableCollection<User> user = new ObservableCollection<User>();
public string FirstName
{
get { return user.FirstName; }
set
{
if (user.FirstName != value)
{
user.FirstName = value;
OnPropertyChange("FirstName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
public string LastName
{
get { return user.LastName; }
set
{
if (user.LastName != value)
{
user.LastName = value;
OnPropertyChange("LastName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
// This property is an example of how model properties can be presented differently to the View.
// In this case, we transform the birth date to the user's age, which is read only.
public int Age
{
get
{
DateTime today = DateTime.Today;
int age = today.Year - user.BirthDate.Year;
if (user.BirthDate > today.AddYears(-age)) age--;
return age;
}
}
// This property is just for display purposes and is a composition of existing data.
public string FullName
{
get { return FirstName + " " + LastName; }
}
public MyViewModel()
{
user = new User
{
FirstName = "John",
LastName = "Doe",
BirthDate = DateTime.Now.AddYears(-30)
};
//outerObservableCollection.Add(user);
//outerObservableCollection.Add(
// new ObservableCollection<User>
// {
// {
// FirstName = "Jane",
// LastName = "June",
// BirthDate = DateTime.Now.AddYears(-20)
// }
// }
//);
);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChange(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
... Please help me. Thank you in advance.
First of all, welcome to C#, WPF, and MVVM!
From your description it sounds like you would like to display a tree of users within groups... with that in mind, you could implement something like this to accomplish that goal:
Models
public class GroupModel
{
public GroupModel(uint id, string name)
{
Id = id;
Name = name;
}
public uint Id { get; }
public string Name { get; }
}
public class UserModel
{
public UserModel(uint id, string firstName, string surname, DateTime dateOfBirth)
{
Id = id;
FirstName = firstName;
Surname = surname;
DateOfBirth = dateOfBirth;
}
public uint Id { get; }
public string FirstName { get; }
public string Surname { get; }
public DateTime DateOfBirth { get; }
}
ViewModels
Base classes
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public abstract class ViewModelBase<TModel> : ViewModelBase
where TModel : class
{
private TModel _model;
public ViewModelBase(TModel model)
=> _model = model;
/*
* There is a design choice here to allow the model
* to be swapped at runtime instead or to treat the
* view model as immutable in which case the setter
* for the Model property should be removed.
*/
public TModel Model
{
get => _model;
set
{
if (ReferenceEquals(_model, value))
{
return;
}
_model = value;
OnPropertyChanged();
}
}
}
Concrete classes
public class GroupViewModel : ViewModelBase<GroupModel>
{
public GroupViewModel(GroupModel model)
: base(model)
{
}
public ObservableCollection<UserViewModel> Users { get; }
= new ObservableCollection<UserViewModel>();
public void AddUser(UserModel user)
{
var viewModel = new UserViewModel(user);
Users.Add(viewModel);
}
}
public class UserViewModel : ViewModelBase<UserModel>
{
public UserViewModel(UserModel model)
: base(model)
{
}
// convenience property; could be done completely in XAML as well
public string FullName => $"{Model.FirstName} {Model.Surname}";
}
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
// create sample user groups
for (var groupIndex = 1u; groupIndex <= 5u; ++groupIndex)
{
var groupName = $"Group {groupIndex}";
var groupModel = new GroupModel(groupIndex, groupName);
var groupViewModel = new GroupViewModel(groupModel);
UserGroups.Add(groupViewModel);
for (var userIndex = 1u; userIndex <= 5u; ++userIndex)
{
var userModel = new UserModel(
id: userIndex,
firstName: "John",
surname: $"Smith",
dateOfBirth: DateTime.Today);
groupViewModel.AddUser(userModel);
}
}
}
public ObservableCollection<GroupViewModel> UserGroups { get; }
= new ObservableCollection<GroupViewModel>();
}
View
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:UserGroups.ViewModels"
x:Class="UserGroups.Views.MainWindow"
Title="User Groups"
Width="1024"
Height="768">
<Window.DataContext>
<viewModels:MainViewModel />
</Window.DataContext>
<Grid>
<TreeView ItemsSource="{Binding Path=UserGroups}">
<TreeView.Resources>
<!-- Template for Groups -->
<HierarchicalDataTemplate DataType="{x:Type viewModels:GroupViewModel}"
ItemsSource="{Binding Path=Users}">
<TextBlock Text="{Binding Path=Model.Name}" />
</HierarchicalDataTemplate>
<!-- Template for Users -->
<DataTemplate DataType="{x:Type viewModels:UserViewModel}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Model.Id, StringFormat='[{0}]'}"
Margin="3,0" />
<TextBlock Text="{Binding Path=FullName}"
Margin="3,0" />
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>
Here's what you end up with:
There are lots of frameworks that take care of a lot of the tedious work of working with the MVVM pattern (e.g. removing most/all of the boilerplate code for INotifyPropertyChanged). Here are just a few to look at:
MVVM Light Toolkit
Prism
ReactiveUI
Some additional resources that might also be useful:
SnoopWPF
WPF Tutorial
It's not entirely clear to me what the result is supposed to look like, but here are some initial suggestions.
I wouldn't try nesting an Observable collection inside another one. Instead, define something like a separate Group model class that has a list of User objects as its field.
I take it the user is supposed to enter values for your bound properties in the xaml in order to create a new user? You need to add a button or something that the user can press after filling those values out. The button click should be bound to a RelayCommand (add MVVMLight to the project if necessary) in the view model. The method invoked by said relaycommand would instantiate a new User object using the fields bound in the xaml and add to your ObservableCollection.
<Button Command="{Binding Path=CreateUserCommand}">
<TextBlock Text="Create User"/>
</Button>
Then in the view model...
public RelayCommand CreateUserCommand { get; private set; }
CreateUserCommand = new RelayCommand(() =>
{
User user = new User
{
FirstName = FirstName,
LastName = LastName,
//...etc.
}
collectionOfUsers.Add(user);
});
"I also don't know how to bind the new users to the XAML."
So far I don't see any xaml code that would handle displaying new users. Seems to me you'd want to bind your collection of users to a grid or combo box. After the user enters new user properties in the textboxes and clicks the appropriate button, the grid or combo box would update. You could have separate controls for separate groups. Again, that part is not entirely clear to me.
Given (3), your ObservableCollection of users needs to be a property in the view model that implements OnPropertyChanged.
Hope that helps.
I am an experienced developer, but relative newcomer to the world of WPF and MVVM. I’ve been reading up on various tutorials and examples of following the MVVM pattern. I am working on converting an existing MDI Windows forms (a student/class management system) application into WPF. My basic design is for a menu (tree view) docked on the left side of the main window with a tab control that would contain the different views (student, class, teacher, billing etc) that the user requires. As proof of concept (and to get my head around WPF) I have the following:
A simple model, Student
public class Student
{
public DateTime BirthDate { get; set; }
public string Forename { get; set; }
public int Id { get; set; }
public string Surname { get; set; }
public override string ToString()
{
return String.Format("{0}, {1}", Surname, Forename);
}
}
The StudentViewModel
public class StudentViewModel : WorkspaceViewModel
{
private Student student;
public override string DisplayName
{
get
{
return String.Format("{0} {1}", student.Forename, student.Surname);
}
}
public string Forename
{
get
{
return student.Forename;
}
set
{
student.Forename = value;
RaisePropertyChanged();
RaisePropertyChanged("DisplayName");
}
}
public int Id
{
get
{
return student.Id;
}
set
{
student.Id = value;
RaisePropertyChanged();
}
}
public string Surname
{
get
{
return student.Surname;
}
set
{
student.Surname = value;
RaisePropertyChanged();
RaisePropertyChanged("DisplayName");
}
}
public StudentViewModel()
{
this.student = new Student();
}
public StudentViewModel(Student student)
{
this.student = student;
}
}
The view model inherits WorkspaceViewModel, an abstract class
public abstract class WorkspaceViewModel : ViewModelBase
{
public RelayCommand CloseCommand { get; set; }
public event EventHandler OnClose;
public WorkspaceViewModel()
{
CloseCommand = new RelayCommand(Close);
}
private void Close()
{
OnClose?.Invoke(this, EventArgs.Empty);
}
}
This in turn inherits ViewModelBase, where I implement INotifyPropertyChanged. The RelayCommand class is a standard implementation of the ICommand interface.
The MainWindowViewModel holds a collection of Workspaces
public class MainViewModel : WorkspaceViewModel
{
private WorkspaceViewModel workspace;
private ObservableCollection<WorkspaceViewModel> workspaces;
public WorkspaceViewModel Workspace
{
get
{
return workspace;
}
set
{
workspace = value;
RaisePropertyChanged();
}
}
public ObservableCollection<WorkspaceViewModel> Workspaces
{
get
{
return workspaces;
}
set
{
workspaces = value;
RaisePropertyChanged();
}
}
public RelayCommand NewTabCommand { get; set; }
public MainViewModel()
{
Workspaces = new ObservableCollection<WorkspaceViewModel>();
Workspaces.CollectionChanged += Workspaces_CollectionChanged;
NewTabCommand = new RelayCommand(NewTab);
}
private void NewTab()
{
Student student = new Student();
StudentViewModel workspace = new StudentViewModel(student);
Workspaces.Add(workspace);
Workspace = workspace;
}
private void Workspaces_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null && e.NewItems.Count != 0)
{
foreach (WorkspaceViewModel workspace in e.NewItems)
{
workspace.OnClose += Workspace_OnClose; ;
}
}
if (e.OldItems != null && e.OldItems.Count != 0)
{
foreach (WorkspaceViewModel workspace in e.OldItems)
{
workspace.OnClose -= Workspace_OnClose;
}
}
}
private void Workspace_OnClose(object sender, EventArgs e)
{
var workspace = (WorkspaceViewModel)sender;
Workspaces.Remove(workspace);
}
}
The StudentView xaml
<UserControl x:Class="MvvmTest.View.StudentView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MvvmTest.View"
xmlns:vm="clr-namespace:MvvmTest.ViewModel"
mc:Ignorable="d">
<UserControl.DataContext>
<vm:StudentViewModel/>
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="ID:"/>
<TextBlock Grid.Column="0" Grid.Row="1" Text="Forename:"/>
<TextBlock Grid.Column="0" Grid.Row="2" Text="Surname:"/>
<TextBlock Grid.Column="0" Grid.Row="3" Text="Date of Birth:"/>
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Id, Mode=TwoWay}"/>
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Forename, Mode=TwoWay}"/>
<TextBox Grid.Column="1" Grid.Row="2" Text="{Binding Surname, Mode=TwoWay}"/>
<DatePicker Grid.Column="1" Grid.Row="3" SelectedDate="{Binding BirthDate, Mode=TwoWay}"/>
</Grid>
</UserControl>
The StudentViewModel and StudentView are linked via a resource dictionary in App.xaml
<ResourceDictionary>
<vm:MainViewModel x:Key="MainViewModel"/>
<DataTemplate DataType="{x:Type vm:StudentViewModel}">
<v:StudentView/>
</DataTemplate>
</ResourceDictionary>
And finally, the MainWindow view (goal is that this will eventually conform to MVVM in that the MainWindowViewModel will define the menu structure)
<Window x:Class="MvvmTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MvvmTest"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:vm="clr-namespace:MvvmTest.ViewModel"
xmlns:v="clr-namespace:MvvmTest.View"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<DockPanel>
<StackPanel DockPanel.Dock="Left" Orientation="Vertical">
<Button Content="New Student">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding NewTabCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
<TabControl ItemsSource="{Binding Workspaces}" SelectedItem="{Binding Workspace}">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding DisplayName, Mode=OneWay}"/>
<Button>X</Button>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<StackPanel>
<UserControl Content="{Binding}"/>
</StackPanel>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</Window>
When I click the ‘New student’ button a new student workspace is created, added to Workspaces collection and displays in the TabControl. All seems well. But when I enter data on the view I noticed that the tab header isn’t updated. First sign that all is not working as it should...
Then when I click ‘New student’ a second time. Another workspace is created, but that duplicates the values entered in the first. Further, when editting the second tab, the first is also updated.
Placing a breakpoint into the NewTab method revealed that although the Workspaces collection holds StudentViewModels, the display properties are still null; even though the StudentView appears to hold data.
After much puzzling I discovered that if I do not set the data context on the StudentView xaml then the binding behaves properly and the test app works as expected. But then doesn't that mean the xaml designer isn't really validating the display property bindings, even though at runtime the path is resolved?
Anyway, I’m now left a few questions. How and why does what I've done work? It essentially appears to go against everything I’ve read and seen on MVVM. Furthermore when trying to apply this application to a MVVM framework (eg MVVM Light) the views are explicitly defined with the data context set in the xaml (eg: DataContext="{Binding Path=Student, Source={StaticResource Locator}}). Which makes even less sense...
As I said, what I’ve got does work, but I’m not really understanding why, and therefore doubt is clawing away that I’ve done something wrong. As a result I’m reluctant to proceed further on serious development from fear of having to rework later (having dug myself into a hole).
Child controls automatically inherit DataContext from their parent. So if no DataContext is specified in the UserControl then each instance uses the instance of StudentViewModel contained in the WorkSpaces Collection. On the other hand when specifing the datacontext in the UserControl XAML each instance of the view is bound the same ViewModel instance. That is why changing data on one view results in changes on all other views. The views are all referencing the same object. I hope that is clear.
I have an ObservableCollection<IRuleCondition> that I want to display - the IRuleCondition interface is used by 2 different classes I want to display, a RuleCondition that simply displays one rule condition (info such as priority, property to check and so on), and a RuleConditionGroup, that can contain 2 or more RuleConditions, grouped in such a way that any of the conditions could match, or all etc.
In the XAML I was wondering is there a way to display a different ListView.ItemTemplate depending on what the type is that it encounters in the ObservableCollection<IRuleCondition>? Or would I need to implement two different ObservableCollections?
Here is a simple example of how this works
This is how the objects are defined
public interface Person
{
string Name { get; set; }
}
public class Manager : Person
{
public string Name { get; set; }
}
public class Employee : Person
{
public string Name { get; set; }
public string ManagerName { get;set;}
}
This is the MainWindow code behind
public partial class MainWindow : Window
{
ObservableCollection<Person> mPeople = new ObservableCollection<Person>();
public ObservableCollection<Person> People
{
get
{
return mPeople;
}
}
public MainWindow()
{
DataContext = this;
mPeople.Add( new Employee{ Name = "x" , ManagerName = "foo"});
mPeople.Add( new Manager { Name = "y"});
InitializeComponent();
}
}
This is the MainWindow XAML
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:WpfApplication1"
Title="MainWindow"
Height="350"
Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type my:Employee}">
<StackPanel Background="Green" Width="300">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding ManagerName}" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type my:Manager}">
<StackPanel Background="Red"
Width="300">
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding People}"></ListBox>
</Grid>
</Window>
As you can see there are two datatemplates one for Manager and one for Employee
And this is how the crappy output looks like. Notice the green and red background and extra field displayed for the Employee compared to the manager
Just define two different DataTemplates in the Resources section, one for each RuleCondition type.
1) Create your two different data templates, just as you say you've already done.
2) Create a custom DataTemplateSelector to choose the appropriate template.
One of your comments states that you're getting an error from your DataTemplateSelector. Verify that you're implementing the class correctly, perhaps paste your implementation. It should be fairly small and straightforward.
I am new to WPF so please accept my apologies if my question is stupid.
I am creating a food ordering system which consists of 2 main sections, the "Food Menu" and the "Order List". When user chooses an item from the food menu, it will be added to the a listbox control which represents the order list.
I have created a few custom objects: Order, OrderLine, Item, as well as a Collection class, OrderLineCollection, for "OrderLine". They look like the following:
public class Order
{
public string ID { get; set; }
public DateTime DateTime { get; set; }
public double TotalAmt { get; set; }
public OrderLineCollection LineItems { get; set; }
}
public class OrderLine
{
public Item Item { get; set; }
public double UnitPrice { get; set; }
public int Quantity { get; set; }
public double SubTotal { get { return unitPrice * quantity; } }
}
[Serializable]
public class OrderLineCollection : CollectionBase
{
public OrderLine this[int index]
{
get { return (OrderLine)List[index]; }
set { List[index] = value; }
}
}
public class Item
{
public string ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public double UnitPrice { get; set; }
public byte[] Image { get; set; }
}
My ListBox control has a DataTemplate so that more details are shown for each item. The XAML as below:
<Page x:Class="FoodOrdering.OrderList"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Order List">
<ListBox Name="lbxOrder" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="30"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Text="{Binding item.name}"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="x "/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding quantity}" Margin="10,0,0,0"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding subTotal, Converter={StaticResource priceConverter}}" HorizontalAlignment="Right"/>
<Button Grid.Row="1" Grid.Column="2" Margin="0,5,0,0" Click="btnDelete_Click">Delete</Button>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Page>
so when items are added, the ListBox will look like the below image:
https://dl.dropbox.com/u/6322828/orderList.png
In the code-behind, I have created a static variable currentOrder for storing the current order and it will be used by other classes/methods.
And every time its value is changed, the following stupid method LoadCurrentOrder() is called to refresh the ListBox view.
public partial class OrderList : Page
{
public static Order currentOrder = new Order();
public OrderList()
{
InitializeComponent();
LoadCurrentOrder();
}
public void LoadCurrentOrder()
{
lbxOrder.Items.Clear();
foreach (OrderLine ol in currentOrder.LineItems)
lbxOrder.Items.Add(ol);
}
}
My problem is how can I bind the data in an elegant way (such as using Resources ItemsSource and so on) instead of using the above method, so that the ListBox will update automatically every time the value of the variable is changed?
I tried
lbxOrder.ItemsSource = currentOrder;
but it does not work as currentOrder is not a System.Collections.IEnumerable object.
1 - Don't create your own collection types, the .Net framework Base Class Library already has pretty much everything you need.
Remove the OrderLineCollection and use an ObservableCollection<OrderLine>.
2 - Try to stick to MVVM conventions. This means you should not manipulate UI elements in code.
public OrderList()
{
InitializeComponent();
LoadCurrentOrder();
DataContext = this;
}
then in XAML:
<ListBox ItemsSource="{Binding LineItems}" HorizontalContentAlignment="Stretch">
3 - Do not name UI elements in XAML unless you need to reference them in XAML. This will help you reconsider your approach every time you find yourself wanting to manipulate UI elements in procedural code.
4 - Get used to the C# naming convention. Property names should be "proper cased" (I.E LineItems instead of lineItems)
All you have to do is to make the lineItems property into an ObservableCollection<yourType>. Then when this collection is changed (items added, removed) the listbox will refresh automatically.
If your problem is that the order itself changes and you need to refresh the listbox. Then all you need to do is to implement INotifyPropertyChanged and when the order changes, to trigger the notification that the property has changed and the UI will refresh via the binding.
As for the binding in a more elegant way. You can bind to the currentOrder.lineItems:
lbxOrder.ItemsSource = currentOrder.lineItems;//this should be an observable collection if you intend to have items change