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.
Related
I get this error:- System.NullReferenceException: 'Object reference not set to an instance of an object.'
objectPlacement was null.
private void Button_Click(object sender, RoutedEventArgs e)
{
ObjectPlacement w = new ObjectPlacement() {Topmost = };// ObjectPlacement is new WPF window
objectPlacement.WindowStyle = WindowStyle.None;
settingpanel.Children.Add(objectPlacement);//settingpanel stack is panel name
w.Show();
}
It would be much more usual to define a usercontrol or datatemplate for whatever you're trying to show in your window. A window is a kind of content control. One way to think of a window ( or contentcontrol ) is something that shows you some UI. All the UI in a window is that content.
When you add window to a project it is templated out with a grid in it.
This is the content and everything you want to see in that window goes in it.
You could replace that grid with something else instead.
If you made that a contentpresenter then you can bind or set what that'll show to some encapsulated re-usable UI.
Usually the best way to encapsulate re-usable UI is as a usercontrol.
A datatemplate can reference a usercontrol.
It is not usually your entire UI for a window you want to switch out. But you can and that is occasionally useful - say if you want a generic way to show dialogs.
The usual way to write wpf is mvvm so most devs will want some mvvm way of switching out UI.
I'll show you some code might make the description clearer.
There are some corners cut in what follows, so this is illustrative. Don't just run with this for your next lead developer interview at a stock traders.
But, basically you click a button for Login you "navigate" to a LoginUC view. Click a button for User and you "navigate" to UserUC.
My mainwindow.
<Window.Resources>
<DataTemplate DataType="{x:Type local:LoginViewModel}">
<local:LoginUC/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:UserViewModel}">
<local:UserUC/>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ItemsControl ItemsSource="{Binding NavigationViewModelTypes}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}"
Command="{Binding DataContext.NavigateCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
CommandParameter="{Binding VMType}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ContentPresenter Grid.Column="1"
Content="{Binding CurrentViewModel}"
/>
</Grid>
</Window>
Notice the datatemplates which associate the type of a viewmodel with a usercontrol.
https://learn.microsoft.com/en-us/dotnet/desktop/wpf/data/data-templating-overview?view=netframeworkdesktop-4.8
What will happen is you present your data in a viewmodel to the UI via that contentpresenter and binding. That viewodel is then templated out into UI with your viewmodel as it's datacontext. The datacontext of a UserUC view will therefore be an instance of UserViewModel. Change CurrentViewModel to an instance of LoginViewModel and you get a LoginUC in your mainwindow instead.
The main viewmodel.
public class MainWindowViewModel : INotifyPropertyChanged
{
public string MainWinVMString { get; set; } = "Hello from MainWindoViewModel";
public ObservableCollection<TypeAndDisplay> NavigationViewModelTypes { get; set; } = new ObservableCollection<TypeAndDisplay>
(
new List<TypeAndDisplay>
{
new TypeAndDisplay{ Name="Log In", VMType= typeof(LoginViewModel) },
new TypeAndDisplay{ Name="User", VMType= typeof(UserViewModel) }
}
);
private object currentViewModel;
public object CurrentViewModel
{
get { return currentViewModel; }
set { currentViewModel = value; RaisePropertyChanged(); }
}
private RelayCommand<Type> navigateCommand;
public RelayCommand<Type> NavigateCommand
{
get
{
return navigateCommand
?? (navigateCommand = new RelayCommand<Type>(
vmType =>
{
CurrentViewModel = null;
CurrentViewModel = Activator.CreateInstance(vmType);
}));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Type and display relates the type for a viewmodel with text displayed in the UI.
public class TypeAndDisplay
{
public string Name { get; set; }
public Type VMType { get; set; }
}
This is "just" quick and dirty code to illustrate a principle which is usually called viewmodel first navigation. Google it, you should find a number of articles explaining it further.
For completeness:
<UserControl x:Class="wpf_Navigation_ViewModelFirst.LoginUC"
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:wpf_Navigation_ViewModelFirst"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel Background="Yellow">
<TextBlock Text="This is the Login User Control"/>
<TextBox>
<TextBox.InputBindings>
<KeyBinding Key="Return" Command="{Binding LoginCommand}"/>
</TextBox.InputBindings>
</TextBox>
</StackPanel>
</UserControl>
public class LoginViewModel
{
private RelayCommand loginCommand;
public RelayCommand LoginCommand
{
get
{
return loginCommand
?? (loginCommand = new RelayCommand(
() =>
{
string s = "";
}));
}
}
}
<UserControl x:Class="wpf_Navigation_ViewModelFirst.UserUC"
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:wpf_Navigation_ViewModelFirst"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Background="pink">
<TextBlock Text="This is the User module Control"
VerticalAlignment="Top"
/>
<TextBlock Text="{Binding Path=DataContext.MainWinVMString, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
VerticalAlignment="Bottom"
/>
</Grid>
</UserControl>
public class UserViewModel
{
}
I put this together some years ago, I would now recommend the community mvvm toolkit with it's code generation, base classes, messenger etc.
I am (new in C# and WPF and) trying to bind data from more sources (class properties) and I am a bit confused from different guides and advices. I want to dynamically add Users in userList and showing for example the last insert and whole list at the same time. That is done on different place in source code, but simple like in contructor of my example. How and where should I set binding and datacontext for those three elements (myName,myTitle and myUserList) to reflect changes in main class properties? Should I call every time function for update binding target, or set this.datacontext after editing properties? Or should I bind to some function (if it's possible) which returns the value I need? I am a bit lost with binding to property of object and datacontexts etc. Here is an example from what I have:
<Window x:Name="Window" x:Class="WpfTest.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:WpfTest">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<StackPanel>
<TextBox x:Name="myName" Text="" Grid.Column="1"/>
<TextBox x:Name="myTitle" Text="" Grid.Column="0"/>
</StackPanel>
<ListBox x:Name="myUserList">
</ListBox>
</Grid>
</Window>
And
public partial class MainWindow : Window {
public User myUser;
public Dictionary<int,User> userList= new Dictionary<int, User>();
public object SubWindow { get; private set; }
public MainWindow() {
newUser = new User();
newUser.Title = "John Doe";
newUser.Name = "Dr.";
this.userList.Add(index,newUser);
this.myUser=newUser;
InitializeComponent();
}
}
And
public class User
{
public String Name { get; set; }
public String Title { get; set; }
}
Thanks for any advice.
First thing is first, WPF works best when you are working with MVVM, the general idea is implementing the INotifyPropertyChanged, what ever items you add to you change, will propagate to the framework and update your views.
When working with Lists, use an ObservableCollection. If you want to add items dynamically to it, you would need to modify the ObservableCollection instead. For best results, in your UserControl, use a DataTemplate for the specific type to display a formatted version of your values.
For the second part, showing the last added item, there are a few ways you can go about this, the best would be to add a new Items(Grid, Stackpanel, etc) that can hold data, use Binding to set its value to a the same context as your list(i.e the ObservableCollection) and create a Converter that will use the ObservableCollection as input, inside your specific converter implementation, just get the last item added and Display it to the control you want( you can use a dataTemplate for this as well)
Solution: You have to bind input data in model class(User) and use model to insert data in the listbox like below
<Window x:Class="WpfRegistration.Listbox"
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:WpfRegistration"
mc:Ignorable="d"
Title="Listbox" Height="450" Width="800">
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.15*"></RowDefinition>
<RowDefinition Height="0.85*"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="63*"></ColumnDefinition>
<ColumnDefinition Width="26*"></ColumnDefinition>
<ColumnDefinition Width="109*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<StackPanel>
<TextBox x:Name="myName" Text="" Grid.Column="1"/>
<TextBox x:Name="myTitle" Text="" Grid.Column="0"/>
</StackPanel>
<StackPanel>
<Button Height="23" Margin="50,40,0,0" Name="button1" VerticalAlignment="Top" HorizontalAlignment="Left" Width="76" Click="Button1_OnClick" >Add Item</Button>
</StackPanel>
<StackPanel>
<Button Height="23" Margin="140,40,10,12" Name="DeleteButton" VerticalAlignment="Top" Click="DeleteButton_Click">Delete Item</Button>
</StackPanel>
<ListBox Grid.Row="1" Grid.Column="0" BorderThickness="3" BorderBrush="Black" Margin="0,60,0,100" x:Name="myUserList">
</ListBox>
</Grid>
</Grid>
Xaml.cs
public partial class Listbox : Window
{
public Listbox()
{
InitializeComponent();
User newUser = new User();
newUser.Title = "John Doe";
newUser.Name = "Dr.";
this.myUserList.Items.Add(newUser.Title + newUser.Name);
}
private void Button1_OnClick(object sender, RoutedEventArgs e)
{
User newUser = new User();
newUser.Title = myTitle.Text;
newUser.Name = myName.Text;
myUserList.Items.Add(newUser.Name + " " + newUser.Title );
myTitle.Text=String.Empty;
myName.Text=String.Empty;
}
private void DeleteButton_Click(object sender, RoutedEventArgs e)
{
myUserList.Items.RemoveAt(myUserList.Items.IndexOf(myUserList.SelectedItem));
}
}
public class User
{
public string name;
public string title;
public String Name
{
get { return name; }
set
{
name = value;
OnPropertyChanged("name");
}
}
public string Title
{
get { return title; }
set
{
title = value;
OnPropertyChanged("title");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I'm having a hard time solving a potential newbie problem: I've got a ObservableCollection<TopItem> MyTopItems that I display in a ListView. The type TopItem contains a string TopName and an ObservableCollection<NestedItem> NestedItems. The type NestedItem contains only a string NestedName.
My problematic is quite simple: I want to retrieve information on the nested item that I select, on the XAML side.
Right now, I can retrieve the selected item of TopItems quite easily, but I can't retrieve the selected item of NestedItems.
I know that I can bind the selected item (for TopItems and NestedItems) in the view model, but in my case it's almost pointless because I've got no use for it in the view model. Plus, I'd really like to know how to do it on the XAML side!
Enough talk, now comes the code.
A class to implement to INotifyPropertyChanged interface that I'm gonna use in my models and view model; not the cleanest way of doing, but it's for the sake of the demo. This class is just there to see the big picture, just know that it works well:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfSelectItemInDoubleList.Utils
{
public abstract class INPCBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisedPropertyChanged([CallerMemberName]string propertyName = null)
{
if (this.PropertyChanged != null)
{
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
{
if (Equals(storage, value))
{
return false;
}
else
{
storage = value;
RaisedPropertyChanged(propertyName);
return true;
}
}
}
}
Comes the NestedItem type:
using WpfSelectItemInDoubleList.Utils;
namespace WpfSelectItemInDoubleList.Model
{
public class NestedItem : INPCBase
{
private string _NestedName;
public string NestedName
{
get { return this._NestedName; }
set
{
SetProperty(ref this._NestedName, value);
}
}
public NestedItem(string nestedName)
{
NestedName = nestedName;
}
}
}
The TopItem type:
using System.Collections.ObjectModel;
using WpfSelectItemInDoubleList.Utils;
namespace WpfSelectItemInDoubleList.Model
{
public class TopItem : INPCBase
{
private string _TopName;
public string TopName
{
get { return this._TopName; }
set
{
SetProperty(ref this._TopName, value);
}
}
private ObservableCollection<NestedItem> _NestedItems;
public ObservableCollection<NestedItem> NestedItems
{
get { return this._NestedItems; }
set
{
SetProperty(ref this._NestedItems, value);
}
}
public TopItem(string topName)
{
TopName = topName;
}
}
}
The view model:
using System.Collections.ObjectModel;
using WpfSelectItemInDoubleList.Model;
using WpfSelectItemInDoubleList.Utils;
namespace WpfSelectItemInDoubleList.ViewModel
{
public class MainWindowViewModel : INPCBase
{
private ObservableCollection<TopItem> _TopItems;
public ObservableCollection<TopItem> TopItems
{
get { return this._TopItems; }
set
{
SetProperty(ref this._TopItems, value);
}
}
public MainWindowViewModel()
{
TopItems = new ObservableCollection<TopItem>();
for (int i = 0; i < 5; i++)
{
var topItem = new TopItem($"top item {i}")
{
NestedItems = new ObservableCollection<NestedItem>()
};
for (int j = 0; j < 5; j++)
{
var nestedItem = new NestedItem($"NI {j}");
topItem.NestedItems.Add(nestedItem);
}
TopItems.Add(topItem);
}
}
}
}
Finally, the most important part: the XAML!:
<Window x:Class="WpfSelectItemInDoubleList.View.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:WpfSelectItemInDoubleList"
xmlns:vm="clr-namespace:WpfSelectItemInDoubleList.ViewModel"
mc:Ignorable="d"
Title="List in list" Height="350" Width="525">
<Window.DataContext>
<vm:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="50" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<ListView x:Name="TopItemsLV" Grid.Row="0" Margin="10" HorizontalContentAlignment="Stretch" ItemsSource="{Binding TopItems, Mode=TwoWay}" SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="TopNameTB" Grid.Column="0" Text="{Binding TopName}" TextWrapping="Wrap" VerticalAlignment="Center" />
<StackPanel Grid.Column="1">
<ListView x:Name="NestedItemsLV" ItemsSource="{Binding NestedItems}" BorderThickness="0" HorizontalAlignment="Center" SelectionMode="Single">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Background="Transparent" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding NestedName}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
<ContentControl Grid.Row="1" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ContentControl Margin="10" Grid.Row="1" Content="{Binding ElementName=TopItemsLV, Path=SelectedItem.TopName}" />
<ContentControl Margin="10" Grid.Row="2" Content="{Binding ElementName=NestedItemsLV, Path=SelectedItem.NestedName}" />
</Grid>
</Window>
The interesting part is the second ContentControl. The first one is working well, but the second doesn't: nothing is showing when I select a nested item. A hint is given to me by intellisense: it sees the TopItemsLV, but not the NestedItemsLV.
Prepare for the most beautiful UI ever. Please don't stole it from me, I'm planning to make millions out of it! Just kidding.
As you can see, the selected item from TopItems is showing, but not the selected item from NestedItems. Any idea why?
Thanks :)
EDIT: Skip the first solution. It's more appropriate for really simple views. Scroll down to my second solution instead.
If you are only binding a single ItemsControl (e.g., ListView) to this list of TopItem instances, then you could just the default collection view manage the selected items for you. That's probably the simplest way to do this.
First, set IsSynchronizedWithCurrentItem="True" on both TopItemsLV and NestedItemsLV.
Then, change your content control bindings as follows:
<ContentControl Content="{Binding Path=TopItems/TopName}" />
<ContentControl Content="{Binding Path=TopItems/NestedItems/NestedName}" />
The / separator in a binding path means "drill down into the currently selected item". The selected item is maintained by the default collection view for both your TopItems collection and each NestedItems collection. The default collection view is what you would get if you called CollectionViewSource.GetDefaultView.
Better Solution
The conventional MVVM approach would be to add a SelectedItem property alongside your TopItems and NestedItems collections. Make sure they fire property change events. The property type should match the corresponding collection's element type. If these properties start out with a null value, then nothing will be selected initially, which is what you want.
On both list views, set SelectedItem="{Binding SelectedItem, Mode=TwoWay}". Remove the IsSynchronizedWithCurrentItem settings from my original answer.
Adjust your content control bindings as follows:
<ContentControl Content="{Binding Path=SelectedItem.TopName}" />
<ContentControl Content="{Binding Path=SelectedItem.SelectedItem.NestedName}" />
Attach a new event handler to NestedItemsLV:
<ListView x:Name="NestedItemsLV"
GotFocus="OnNestedItemsLVGotFocus"
... />
In your view's code-behind, implement the handler as follows:
private void OnNestedItemsLVGotFocus(object sender, RoutedEventArgs e)
{
var viewModel = this.DataContext as MainWindowViewModel;
var parentItem = (sender as FrameworkElement)?.DataContext as TopItem;
if (viewModel != null && parentItem != null)
viewModel.SelectedItem = parentItem;
}
I think you'll agree that this solution works better.
I have parent window which has textBox called "SchoolName", and a button called "Lookup school Name".
That Button opens a child window with list of school names. Now when user selects school Name from child window, and clicks on "Use selected school" button. I need to populate selected school in parent view's textbox.
Note: I have adopted Sam’s and other people’s suggestion to make this code work. I have updated my code so other people can simply use it.
SelectSchoolView.xaml (Parent Window)
<Window x:Class="MyProject.UI.SelectSchoolView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Parent" Height="202" Width="547">
<Grid>
<TextBox Height="23" Width="192"
Name="txtSchoolNames"
Text="{Binding Path=SchoolNames, UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}"
/>
<Label Content="School Codes" Height="28" HorizontalAlignment="Left"
Margin="30,38,0,0" Name="label1" VerticalAlignment="Top" />
<Button Content="Lookup School Code" Height="30" HorizontalAlignment="Left"
Margin="321,36,0,0" Name="button1" VerticalAlignment="Top" Width="163"
Command="{Binding Path=DisplayLookupDialogCommand}"/>
</Grid>
</Window>
SchoolNameLookup.xaml (Child Window for Look up School Name)
<Window x:Class="MyProject.UI.SchoolNameLookup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"
Title="SchoolCodeLookup" Height="335" Width="426">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="226*" />
<RowDefinition Height="70*" />
</Grid.RowDefinitions>
<toolkit:DataGrid Grid.Row="0" Grid.Column="1" x:Name="dgSchoolList"
ItemsSource="{Binding Path=SchoolList}"
SelectedItem="{Binding Path=SelectedSchoolItem, Mode=TwoWay}"
Width="294"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserResizeRows="False"
CanUserSortColumns="True"
SelectionMode="Single">
<Button Grid.Row="1" Grid.Column="1" Content="Use Selected School Name"
Height="23" Name="btnSelect" Width="131" Command="{Binding
Path=UseSelectedSchoolNameCommand}" />
</Grid>
</Window>
SchoolNameLookupViewModel
private string _schoolNames;
public string SchoolNames
{
get { return _schoolNames; }
set
{
_schoolNames= value;
OnPropertyChanged(SchoolNames);
}
}
private ICommand _useSelectedSchoolNameCommand;
public ICommand UseSelectedSchoolNameCommand{
get
{
if (_useSelectedSchoolNameCommand== null)
_useSelectedSchoolNameCommand= new RelayCommand(a =>
DoUseSelectedSchollNameItem(), p => true);
return _useSelectedSchoolNameCommand;
}
set
{
_useSelectedSchoolNameCommand= value;
}
}
private void DoUseSelectedSchoolNameItem() {
StringBuilder sfiString = new StringBuilder();
ObservableCollection<SchoolModel> oCol =
new ObservableCollection<SchoolModel>();
foreach (SchoolModel itm in SchollNameList)
{
if (itm.isSelected) {
sfiString.Append(itm.SchoolName + "; ");
_schoolNames = sfiString.ToString();
}
}
OnPropertyChanged(SchoolNames);
}
private ICommand _displayLookupDialogCommand;
public ICommand DisplayLookupDialogCommand
{
get
{
if (_displayLookupDialogCommand== null)
_displayLookupDialogCommand= new
RelayCommand(a => DoDisplayLookupDialog(), p => true);
return _displayLookupDialogCommand;
}
set
{
_displayLookupDialogCommand= value;
}
}
private void DoDisplayLookupDialog()
{
SchoolNameLookup snl = new SchoolNameLookup();
snl.DataContext = this; //==> This what I was missing. Now my code works as I was expecting
snl.Show();
}
My solution is to bind both the windows to the same ViewModel, then define a property to hold the resulting value for codes, lets call it CurrentSchoolCodes, Bind the label to this property. Make sure that CurrentSchoolCodes raises the INotifyPropertyChanged event.
then in the DoUseSelectedSchoolNameItem set the value for CurrentSchoolCodes.
For properties in your models I suggest you to load them as they are required(Lazy Load patttern). I this method your property's get accessor checks if the related field is still null, loads and assigns the value to it.
The code would be like this code snippet:
private ObservableCollection<SchoolModel> _schoolList;
public ObservableCollection<SchoolModel> SchoolList{
get {
if ( _schoolList == null )
_schoolList = LoadSchoolList();
return _schoolList;
}
}
In this way the first time your WPF control which is binded to this SchoolList property tries to get the value for this property the value will be loaded and cached and then returned.
Note: I have to say that this kind of properties should be used carefully, since loading data could be a time consuming process. And it is better to load data in a background thread to keep UI responsive.
The Solution Sam suggested here is a correct one.
What you didn't get is that you should have only one instance of you viewmodel and your main and child page should refer to the same one.
Your viewmodel should be instanciated once: maybe you need a Locator and get the instance there... Doing like this the code in your ctor will fire once, have a look at the mvvmLight toolkit, I think it will be great for your usage, you can get rid of those Classes implementing ICommand too...
You can find a great example of using that pattern here:
http://blogs.msdn.com/b/kylemc/archive/2011/04/29/mvvm-pattern-for-ria-services.aspx
basically what happens is this:
you have a Locator
public class ViewModelLocator
{
private readonly ServiceProviderBase _sp;
public ViewModelLocator()
{
_sp = ServiceProviderBase.Instance;
// 1 VM for all places that use it. Just an option
Book = new BookViewModel(_sp.PageConductor, _sp.BookDataService);
}
public BookViewModel Book { get; set; }
//get { return new BookViewModel(_sp.PageConductor, _sp.BookDataService); }
// 1 new instance per View
public CheckoutViewModel Checkout
{
get { return new CheckoutViewModel(_sp.PageConductor, _sp.BookDataService); }
}
}
that Locator is a StaticResource, in App.xaml
<Application.Resources>
<ResourceDictionary>
<app:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
</ResourceDictionary>
</Application.Resources>
in your views you refer you viewmodels trough the Locator:
DataContext="{Binding Book, Source={StaticResource Locator}}"
here Book is an instance of BookViewModel, you can see it in the Locator class
BookViewModel has a SelectedBook:
private Book _selectedBook;
public Book SelectedBook
{
get { return _selectedBook; }
set
{
_selectedBook = value;
RaisePropertyChanged("SelectedBook");
}
}
and your child window should have the same DataContext as your MainView and work like this:
<Grid Name="grid1" DataContext="{Binding SelectedBook}">
I can get this working with an XmlDataSource but not with my own classes. All I want to do is bind the listbox to my collection instance and then link the textbox to the listbox so I can edit the person's name (two-way). I've deliberately kept this as simple as possible in the hope that somebody can fill in the blanks.
XAML:
<Window x:Class="WpfListTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfListTest"
Title="Window1" Height="300" Width="600">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="3"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<DockPanel Grid.Column="0">
<ListBox />
</DockPanel>
<DockPanel Grid.Column="2">
<StackPanel>
<Label>Name</Label>
<TextBox />
</StackPanel>
</DockPanel>
</Grid>
</Window>
C# code behind:
namespace WpfListTest
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public People MyPeeps = new People();
public Window1()
{
InitializeComponent();
MyPeeps.Add(new Person("Fred"));
MyPeeps.Add(new Person("Jack"));
MyPeeps.Add(new Person("Jill"));
}
}
public class Person
{
public string Name { get; set; }
public Person(string newName)
{
Name = newName;
}
}
public class People : List<Person>
{
}
}
All the examples on the web seem to have what is effectively a static class returning code-defined data (like return new Person("blah blah")) rather than my own instance of a collection - in this case MyPeeps. Or maybe I'm not uttering the right search incantation.
One day I might make a sudden breakthrough of understanding this binding stuff but at the moment it's baffling me. Any help appreciated.
The correct way would be to use the MVVM pattern and create a ViewModel like so:
public class MainWindowViewModel : INotifyPropertyChanged
{
private People _myPeeps;
private Person _selectedPerson;
public event PropertyChangedEventHandler PropertyChanged;
public People MyPeeps
{
get { return _myPeeps; }
set
{
if (_myPeeps == value)
{
return;
}
_myPeeps = value;
RaisePropertyChanged("MyPeeps");
}
}
public Person SelectedPerson
{
get { return _selectedPerson; }
set
{
if (_selectedPerson == value)
{
return;
}
_selectedPerson = value;
RaisePropertyChanged("SelectedPerson");
}
}
private void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Initialize it in your View's code behind like so:
public partial class MainWindow : Window
{
private readonly MainWindowViewModel _viewModel;
public MainWindow()
{
_viewModel = new MainWindowViewModel();
_viewModel.MyPeeps = new People();
_viewModel.MyPeeps.Add(new Person("Fred"));
_viewModel.MyPeeps.Add(new Person("Jack"));
_viewModel.MyPeeps.Add(new Person("Jill"));
DataContext = _viewModel;
InitializeComponent();
}
}
And bind the data like so:
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Height="350"
Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160" />
<ColumnDefinition Width="3" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<DockPanel Grid.Column="0">
<ListBox SelectedItem="{Binding SelectedPerson}"
DisplayMemberPath="Name"
ItemsSource="{Binding MyPeeps}" />
</DockPanel>
<DockPanel Grid.Column="2">
<StackPanel>
<Label>Name</Label>
<TextBox Text="{Binding SelectedPerson.Name}" />
</StackPanel>
</DockPanel>
</Grid>
</Window>
The binding will work like this:
The DataContext of the window itself is set to the ViewModel instance. Because the ListBox and the TextBox don't specify any DataContext, they inherit it from the Window. The bindings on an object always work relative to the DataContext if nothing else is being specified. That means that the TextBox binding looks for a property SelectedPerson in its DataContext (i.e., in the MainWindowViewModel) and for a Property Name in that SelectedPerson.
The basic mechanics of this sample are as follows:
The SelectedPerson property on the ViewModel is always synchronized with the SelectedItem of the ListBox and the Text property of the TextBox is always synchronized with the Name property of the SelectedPerson.
Try to inherit your People class from ObservableCollection<Person>