Binding C# WPF controls to a dictionary - c#

Hi :) I’m just learning C# and WPF and I need to write a tool that:
Loads a data file (string Key, int Value)
Binds the data to a WPF UI for edits
Saves a new data file
My thought was a dictionary would be best.
The data is only loaded once from file, and changes are only made with the WPF controls.
I’ve tried many things but I still keep hitting road blocks when I bind data to the controls.
I’ve been working with a simplified version of the tool – below.
The data binds to the WPF control – but there is no change event to update the dictionary.
I haven’t found a good example to follow.
Could someone explain how to get the dictionary to update?
And is the strategy the right one? - using a dictionary -using DataContext.
If you'd like to see the full project and UI - there is a link at the bottom.
I've been working many-many days...with progress but I'm way too slow ;)
Cheers
Danny
MainWindow.xaml
<Window x:Class="configGen.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="200" Width="150">
<StackPanel Margin="20" Width="80">
<TextBox Text="{Binding [item1]}" />
<TextBlock Text="{Binding [item1]}" />
<TextBox Text="{Binding [item2]}" />
<TextBlock Text="{Binding [item2]}" />
<TextBox Text="{Binding [item3]}" />
<TextBlock Text="{Binding [item3]}" />
<Slider Value="{Binding [item4]}" Minimum="0" Maximum="256" />
<TextBlock Text="{Binding [item4]}" />
</StackPanel>
MainWindow.xaml.cs
namespace configGen
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
dataClass record = new dataClass();
DataContext = record.generate();
}
}
public class dataClass
{
public Dictionary<string, int> generate()
{
Dictionary<string, int> _data = new Dictionary<string, int>();
_data.Add("item1", 100);
_data.Add("item2", 120);
_data.Add("item3", 140);
_data.Add("item4", 160);
return _data;
}
}
}
Link to full project...
http://www.baytower.ca/btsRV7config.zip
Thanks for all the great feedback everyone!!
I will set back to work :)

Instead of using a Dictionary as your DataContext I'd create a custom object like MainViewModel. Give it properties that correspond to item1, item2, etc, except give them appropriate names. Then use <TextBox Text="{Binding MyPropertyName}" />. To handle updates, you can either set your DataContext to a new MainViewModel object or you can set up your class to broadcast property changes. You can do that either through INotifyPropertyChanged on the class or with dependency properties.
At least that's what it seems like you're trying to accomplish. If you're going for displaying an arbitrary number of controls you'd need something different.

A dictionary is definitly not a convenient way to do a two way data binding in WPF. It seems an ObservableCollection is more suited to your requirements.
Something like:
public class ItemsList : ObservableCollection<Item>
{
public ItemsList() : base()
{
Add(new Item("item 1", 100));
Add(new Item("item 2", 120));
Add(new Item("item 3", 140));
Add(new Item("item 4", 160));
}
}
Item is a simple class with a name and a value properties. I have ommitted it here because it is self explanatory.
The advantage here is that you can bind to a dynamic number of items not only the ones declared imperatively.
Once you bind you datacontext to it, you get the automatic property notification for two way databinding.
Your XAML will have to change to accomodate binding to a collection of course. Maybe an ItemsControl that takes that collection as its ItemsSource.

Here is an example of how I would do it.
Main Class (Code Behind)
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private List<MyObject> _myObjects;
public List<MyObject> MyObjects
{
get { return _myObjects; }
set
{
_myObjects = value;
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("MyObjects"));
}
}
}
public MainWindow()
{
MyObjects = new List<MyObject>();
// Add 20 records for sample data
for (int i = 0; i < 20; i++)
{
MyObject o = new MyObject();
o.Label = string.Format("Key{0}", i);
o.MyValue = string.Format("Value{0}", i);
MyObjects.Add(o);
}
InitializeComponent();
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
}
Secondary Class
public class MyObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _label;
public string Label
{
get { return _label; }
set
{
_label = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Label"));
}
}
}
private string _myValue;
public string MyValue
{
get
{
return _myValue;
}
set
{
_myValue = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("MyValue"));
}
}
}
}
XAML File
<Window x:Class="WpfApplication4.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">
<Window.Resources>
<DataTemplate x:Key="listboxstyle">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Path=Label}" />
<TextBox Grid.Column="1" Text="{Binding Path=MyValue}" />
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox
ItemsSource="{Binding Path=MyObjects}"
ItemTemplate="{StaticResource listboxstyle}"
/>
</Grid>
</Window>

I've tried tsell's example, using his class in a list. The list is just a convenient way to generate and manage a fixed number of elements. The Item1 WPF control binds to the Item1 object and its value. The object is found by its index number. The binding and dataContext in this case is simple enough for me to use (as a beginner). It works, but I'm not sure it's exactly an elegant way to do it.
public MainWindow()
{
MyObjects = new List<MyObject>();
MyObject item1 = new MyObject();
item1.MyValue = string.Format("100");
MyObjects.Add(item1);
MyObject item2 = new MyObject();
item2.MyValue = string.Format("120");
MyObjects.Add(item2);
MyObject item3 = new MyObject();
item3.MyValue = string.Format("140");
MyObjects.Add(item3);
MyObject item4 = new MyObject();
item4.MyValue = string.Format("160");
MyObjects.Add(item4);
InitializeComponent();
DataContext = this;
}
xaml
<Window x:Class="WpfApplication4.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">
<StackPanel Margin="20" Width="80">
<TextBox Text="{Binding MyObjects[0].MyValue}" />
<TextBlock Text="{Binding MyObjects[0].MyValue}" />
<TextBox Text="{Binding MyObjects[1].MyValue}" />
<TextBlock Text="{Binding MyObjects[1].MyValue}" />
<TextBox Text="{Binding MyObjects[2].MyValue}" />
<TextBlock Text="{Binding MyObjects[2].MyValue}" />
<Slider Value="{Binding MyObjects[3].MyValue}" Minimum="0" Maximum="256" />
<TextBlock Text="{Binding MyObjects[3].MyValue}" />
</StackPanel>
</Window>
BTW, I will change to int for MyValues..they are all int numbers. For now it is a string.

Related

WPF: access ElementName in a list within a list on the XAML side

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.

How to separate View data and ViewModel data?

I need to manage UI specific parameters (View) and Application data (Model/ViewModel) separately, so I'm using the code-behind of the View for the first, and a separated class (prefixed ViewModel) for the later. This is an simplified version of what I have:
View (XAML)
<Window x:Class="UrSimulator.View.MyView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MyView" Height="300" Width="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding FirstColumnWidth}" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel>
<Label>Width:</Label>
<TextBox Text="{Binding FirstColumnWidth}" IsReadOnly="True" Background="LightGray" />
</StackPanel>
<StackPanel Grid.Column="1">
<Label>First Column Width:</Label>
<TextBox Text="{Binding FirstColumnWidth}" />
<Label>View Model Data:</Label>
<TextBox Text="{Binding MyViewModel.PropertyFromVM}" />
<Label Content="{Binding MyViewModel.PropertyFromVM}" />
</StackPanel>
</Grid>
View (Code behind)
public partial class MyView : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private MyViewModel m_MyViewModel;
public MyViewModel MyViewModel
{
get { return m_MyViewModel; }
set
{
m_MyViewModel = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("MyViewModel"));
}
}
private GridLength m_FirstColumnWidth;
public GridLength FirstColumnWidth
{
get { return m_FirstColumnWidth; }
set
{
m_FirstColumnWidth = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("FirstColumnWidth"));
}
}
public MyView()
{
MyViewModel = new MyViewModel();
DataContext = this;
FirstColumnWidth = new GridLength(100);
InitializeComponent();
}
}
ViewModel
public class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string m_PropertyFromVM;
public string PropertyFromVM
{
get { return m_PropertyFromVM; }
set
{
m_PropertyFromVM = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("PropertyFromVM"));
}
}
public MyViewModel()
{
PropertyFromVM = "Some business data";
}
}
It works, but I find it cumbersome to use MyViewModel. on every binding that points to the VM.
Questions:
Is there another way to do this without using the prefix?
How should I write the binding for the UI (the width property) if instead of using this for the DataContext, I'd use:
DataContext = MyViewModel;
I'm doing everything wrong and this is not how it is intended to be?
Note: Forget about the converter needed for the Width, it works as long as the text is valid and is not my concern on the question.
DataContext = this;
Yuck... :)
Let the view model be the data context, and bind on your view's properties like this :
<Window x:Name="This" ...>
...
<SomeControl SomeProperty="{Binding MyViewProperty, ElementName=This}"/>
...
</Window>
Side note :
class MyView : Window, INotifyPropertyChanged
Why aren't your view's properties "dependency properties" if you inherit Window ?
This is one way of doing MVVM, but not a great choice as you are still using tightly coupled View objects.
The ideal is where you let WPF infer what View class to use by binding your ViewModel objects to the Content property of ContentPresenters and setting up DataTemplate entries for your ViewModel types.
That way, you don't even need to use a DataContext = blah statement in your code anywhere.
e.g. in the App.xaml or similar
<DataTemplate DataType={x:Type MyViewModel}>
<local:MyViewModelView/>
</DataTemplate>
... then in the Window/UserControl/XAML wherever you need it...
<ContentPresenter Content={Binding MyViewModelAsAProperty}/>
...which can be a DependencyProperty or a standard INotifyPropertyChanged enabled property on another ViewModel.
Add this codes on your MyViewModel Class
private MyView _ObjMyViewModel;
public MyView ObjMyViewModel
{
get { return _ObjMyViewModel; }
set
{
_ObjMyViewModel= value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("ObjMyViewModel"));
}
}
And in XAML
<Window.DataContext>
<ViewModel:MyViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding ObjMyViewModel.FirstColumnWidth}" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel>
<Label>Width:</Label>
<TextBox Text="{Binding ObjMyViewModel.FirstColumnWidth}" IsReadOnly="True" Background="LightGray" />
</StackPanel>
<StackPanel Grid.Column="1">
<Label>First Column Width:</Label>
<TextBox Text="{Binding ObjMyViewModel.FirstColumnWidth}" />
<Label>View Model Data:</Label>
<TextBox Text="{Binding MyViewModel.PropertyFromVM}" />
<Label Content="{Binding MyViewModel.PropertyFromVM}" />
</StackPanel>
I hope its working..

Dynamic user control change - WPF

I'm developing an app in WPF and I need to change in runtime a content of a ContentControl depending than the user selected on ComboBox.
I have two UserControls and at my combo exists two itens, corresponding each one each.
First usercontrol:
<UserControl x:Class="Validator.RespView"
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"
mc:Ignorable="d"
d:DesignHeight="167" d:DesignWidth="366" Name="Resp">
<Grid>
<CheckBox Content="CheckBox" Height="16" HorizontalAlignment="Left" Margin="12,12,0,0" Name="checkBox1" VerticalAlignment="Top" />
<ListBox Height="112" HorizontalAlignment="Left" Margin="12,43,0,0" Name="listBox1" VerticalAlignment="Top" Width="168" />
<Calendar Height="170" HorizontalAlignment="Left" Margin="186,0,0,0" Name="calendar1" VerticalAlignment="Top" Width="180" />
</Grid>
Second usercontrol:
<UserControl x:Class="Validator.DownloadView"
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"
mc:Ignorable="d"
d:DesignHeight="76" d:DesignWidth="354" Name="Download">
<Grid>
<Label Content="States" Height="28" HorizontalAlignment="Left" Margin="12,12,0,0" Name="label1" VerticalAlignment="Top" />
<ComboBox Height="23" HorizontalAlignment="Left" Margin="12,35,0,0" Name="comboBox1" VerticalAlignment="Top" Width="120" />
<RadioButton Content="Last 48 hs" Height="16" HorizontalAlignment="Left" Margin="230,42,0,0" Name="rdbLast48" VerticalAlignment="Top" />
<Label Content="Kind:" Height="28" HorizontalAlignment="Left" Margin="164,12,0,0" Name="label2" VerticalAlignment="Top" />
<RadioButton Content="General" Height="16" HorizontalAlignment="Left" Margin="165,42,0,0" Name="rdbGeral" VerticalAlignment="Top" />
</Grid>
At MainWindowView.xaml
<Window x:Class="Validator.MainWindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:du="clr-namespace:Validator.Download"
xmlns:resp="clr-namespace:Validator.Resp"
Title="Validator" Height="452" Width="668"
WindowStartupLocation="CenterScreen" ResizeMode="NoResize">
<Window.Resources>
<DataTemplate DataType="{x:Type du:DownloadViewModel}">
<du:DownloadView/>
</DataTemplate>
<DataTemplate DataType="{x:Type resp:RespViewModel}">
<resp:RespView/>
</DataTemplate>
</Window.Resources>
<Grid>
<ComboBox ItemsSource="{Binding Path=PagesName}"
SelectedValue="{Binding Path=CurrentPageName}"
HorizontalAlignment="Left" Margin="251,93,0,0"
Name="cmbType"
Width="187" VerticalAlignment="Top" Height="22"
SelectionChanged="cmbType_SelectionChanged_1" />
<ContentControl Content="{Binding CurrentPageViewModel}" Height="171" HorizontalAlignment="Left" Margin="251,121,0,0" Name="contentControl1" VerticalAlignment="Top" Width="383" />
</Grid>
</Window>
I assigned to the DataContext of the MainView, the viewmodel below:
public class MainWindowViewModel : ObservableObject
{
#region Fields
private ICommand _changePageCommand;
private ViewModelBase _currentPageViewModel;
private ObservableCollection<ViewModelBase> _pagesViewModel = new ObservableCollection<ViewModelBase>();
private readonly ObservableCollection<string> _pagesName = new ObservableCollection<string>();
private string _currentPageName = "";
#endregion
public MainWindowViewModel()
{
this.LoadUserControls();
_pagesName.Add("Download");
_pagesName.Add("Resp");
}
private void LoadUserControls()
{
Type type = this.GetType();
Assembly assembly = type.Assembly;
UserControl reso = (UserControl)assembly.CreateInstance("Validator.RespView");
UserControl download = (UserControl)assembly.CreateInstance("Validator.DownloadView");
_pagesViewModel.Add(new DownloadViewModel());
_pagesViewModel.Add(new RespViewModel());
}
#region Properties / Commands
public ICommand ChangePageCommand
{
get
{
if (_changePageCommand == null)
{
_changePageCommand = new RelayCommand(
p => ChangeViewModel((IPageViewModel)p),
p => p is IPageViewModel);
}
return _changePageCommand;
}
}
public ObservableCollection<string> PagesName
{
get { return _pagesName; }
}
public string CurrentPageName
{
get
{
return _currentPageName;
}
set
{
if (_currentPageName != value)
{
_currentPageName = value;
OnPropertyChanged("CurrentPageName");
}
}
}
public ViewModelBase CurrentPageViewModel
{
get
{
return _currentPageViewModel;
}
set
{
if (_currentPageViewModel != value)
{
_currentPageViewModel = value;
OnPropertyChanged("CurrentPageViewModel");
}
}
}
#endregion
#region Methods
private void ChangeViewModel(IPageViewModel viewModel)
{
int indexCurrentView = _pagesViewModel.IndexOf(CurrentPageViewModel);
indexCurrentView = (indexCurrentView == (_pagesViewModel.Count - 1)) ? 0 : indexCurrentView + 1;
CurrentPageViewModel = _pagesViewModel[indexCurrentView];
}
#endregion
}
On MainWindowView.xaml.cs, I wrote this event to do the effective change:
private void cmbType_SelectionChanged_1(object sender, SelectionChangedEventArgs e)
{
MainWindowViewModel element = this.DataContext as MainWindowViewModel;
if (element != null)
{
ICommand command = element.ChangePageCommand;
command.Execute(null);
}
}
The app run ok and I inspected the application with WPFInspector and saw that the view changes when the combobox is changed internally, but the ContentControl still empty visually..
Sorry about the amount of code that I posted and my miss of knowledge but I'm working with this a long time and can't solve this problem.
Thanks
Issues:
Firstly don't ever create View related stuff in the ViewModel (UserControl). This is no longer MVVM when you do that.
Derive ViewModels from ViewModelBase and not ObservableObject unless you have a compelling reason to not use ViewModelBase when using MVVMLight. Keep ObservableObject inheritence for Models. Serves as a nice separation between VM's and M's
Next you do not need to make everything an ObservableCollection<T> like your _pagesViewModel. You do not have that bound to anything in your View's so it's just a waste. Just keep that as a private List or array. Check what a type actually does in difference to a similar other one.
Not sure about this one, maybe you pulled this code snippet as a demo, but do not use margins to separate items in a Grid. Your Layout is essentially just 1 Grid cell and the margins have the items not overlap. If you're not aware of that issue, Check into WPF Layout Articles.
Please don't forget principles of OOP, Encapsulation and sorts when writing a UI app. When having Properties like CurrentPageViewModel which you don't intend the View to switch make the property setter private to enforce that.
Don't resort to code-behind in the View too soon. Firstly check if it's only a View related concern before doing so. Am talking about your ComboBox SelectionChanged event handler. Your purpose of that in this demo is to switch the Bound ViewModel which is held in the VM. Hence it's not something that the View is solely responsible for. Thus look for a VM involved approach.
Solution:
You can get a working example of your code with the fixes for above from Here and try it out yourself.
Points 1 -> 5 are just basic straightforward changes.
For 6, I've created a SelectedVMIndex property in the MainViewModel which is bound to the SelectedIndex of the ComboBox. Thus when the selected index flips, the property setter after updating itself updates the CurrentPageViewModel as well such as
public int SelectedVMIndex {
get {
return _selectedVMIndex;
}
set {
if (_selectedVMIndex == value) {
return;
}
_selectedVMIndex = value;
RaisePropertyChanged(() => SelectedVMIndex);
CurrentPageViewModel = _pagesViewModel[_selectedVMIndex];
}
}

How to access objects in code from XAML

I am new to WPF and am trying to understand how to use data binding to bind the controls on my window to objects in my code behind. I see several questions about accessing XAML objects from the codebehind, but that's not what I'm looking for. I already know how to do that.
label1.Content = LabelText;
listbox1.ItemsSource = ListItems;
I have also seen answers about how to access a class in the codebehind from XAML.
<local:MyClass x:Key="myClass" />
But I don't see how to apply that to a specific instance of the class. Here is an example of what I'm trying to do. The 'Bindings' are obviously incorrect. That is what I need help with.
public partial class MainWindow : Window
{
private string _labelText;
private List<string> _listItems = new List<string>();
public MainWindow()
{
InitializeComponent();
_labelText = "Binding";
_listItems.Add("To");
_listItems.Add("An");
_listItems.Add("Object");
}
public string LabelText
{
get { return _labelText; }
set { _labelText = value; }
}
public List<string> ListItems
{
get { return _listItems; }
set { _listItems = value; }
}
}
<Window x:Class="SO_Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="SO Demo" Height="160" Width="225">
<Grid DataContext="MainWindow">
<Label x:Name="label1" Width="80" Height="25" Margin="12,12,0,0"
Content="{Binding Path=LabelText}"
HorizontalAlignment="Left" VerticalAlignment="Top" />
<ListBox x:Name="listbox1" Width="100" Height="60" Margin="12,44,0,0"
ItemsSource="{Binding Path=ListItems}" DisplayMemberPath="ListItems"
HorizontalAlignment="Left" VerticalAlignment="Top" />
</Grid>
</Window>
The books and tutorials I have read make it sound like this should be very simple. What am I missing?
While you can DataBind directly to the class in the manner you're attempting, it is not how this is commonly done. The recommended approach is to create an object (ViewModel) that aggregates all the model data you want displayed in your UI, and then set that ViewModel as the DataContext of your View (Window in this case). I would recommend reading about MVVM, which is how most WPF application are built. But the example below can get you started.
Here is a simple example based on your sample above:
ViewModel
public class MyViewModel : INotifyPropertyChanged
{
private string _title;
private ObservableCollection<string> _items;
public string LabelText
{
get { return _title; }
set
{
_title = value;
this.RaisePropertyChanged("Title");
}
}
public ObservableCollection<string> ListItems {
get { return _items; }
set
{
_items = value; //Not the best way to populate your "items", but this is just for demonstration purposes.
this.RaisePropertyChanged("ListItems");
}
}
//Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
CodeBehind
public partial class MainWindow : Window
{
private MyViewModel _viewModel;
public MainWindow()
{
InitializeComponent();
_viewModel = new MyViewModel();
//Initialize view model with data...
this.DataContext = _viewModel;
}
}
View (Window)
<Window x:Class="SO_Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="SO Demo" Height="160" Width="225">
<Grid>
<Label x:Name="label1" Width="80" Height="25" Margin="12,12,0,0" Content="{Binding Path=LabelText}"
HorizontalAlignment="Left" VerticalAlignment="Top" />
<ListBox x:Name="listbox1" Width="100" Height="60" Margin="12,44,0,0"
ItemsSource="{Binding Path=ListItems}"
HorizontalAlignment="Left" VerticalAlignment="Top" />
</Grid>
</Window>
<Grid DataContext="MainWindow"> is invalid.
If you want to reference the window you must either:
<Window x:Name="MyWindow">
<Grid DataContext="{Binding ElementName=MyWindow}"/>
</Window>
or
<Grid DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>

Passing value from child window to parent window using WPF and MVVM pattern

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}">

Categories