I am having a surprising difficulty trying to make a simple thing work, that is, setting a property in a method called by a Command bound to a Button.
When I set the property in the ViewModel constructor, the correct value is properly displayed in View, but when I set this property with the command's method, the View doesn't update, although any breakpoint I create is reached (even inside RaisePropertyChanged in my ViewModelBase). I am using vanilla RelayCommand found easily in online tutorials (from Josh Smith if I am not mistaken).
My project can be downloaded here (Dropbox);
Some important code blocks are below:
ViewModel:
public class IdiomaViewModel : ViewModelBase
{
public String Idioma {
get { return _idioma; }
set {
_idioma = value;
RaisePropertyChanged(() => Idioma);
}
}
String _idioma;
public IdiomaViewModel() {
Idioma = "nenhum";
}
public void Portugues () {
Idioma = "portu";
}
private bool PodePortugues()
{
if (true) // <-- incluir teste aqui!!!
return true;
return false;
}
RelayCommand _comando_portugues;
public ICommand ComandoPortugues {
get {
if (_comando_portugues == null) {
_comando_portugues = new RelayCommand(param => Portugues(),
param => PodePortugues());
}
return _comando_portugues;
}
}
public void Ingles () {
Idioma = "ingle";
}
private bool PodeIngles()
{
if (true) // <-- incluir teste aqui!!!
return true;
return false;
}
RelayCommand _comando_ingles;
public ICommand ComandoIngles {
get {
if (_comando_ingles == null) {
_comando_ingles = new RelayCommand(param => Ingles(),
param => PodeIngles());
}
return _comando_ingles;
}
}
}
View with no extra code behind:
<Window x:Class="TemQueFuncionar.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:app="clr-namespace:TemQueFuncionar"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<app:IdiomaViewModel/>
</Window.DataContext>
<StackPanel>
<Button Content="Ingles" Command="{Binding ComandoIngles, Mode=OneWay}"/>
<Button Content="Portugues" Command="{Binding ComandoPortugues, Mode=OneWay}"/>
<Label Content="{Binding Idioma}"/>
</StackPanel>
</Window>
Youdid fill the Interface implementation put you did not mention it to the base view model.
You are missing this : INotifyPropertyChanged linking Interface to the base class, this makes the the View refreshes the content.
You missed the statement ViewModelBase:INotifyPropertyChanged on ViewModelBase
Related
I have two TextBoxes inside my View on which I am trying to implement a simple validation using MVVM design pattern.The issue is even when my ViewModel is implementing Inotification Changed interface and the property is bound tot the text property of the TextBox,on entering text propertyChange event never fires.I don't know where I have gone wrong.Please help.Its been bugging me for quite a while.
ViewModel :
class TextBoxValidationViewModel : ViewModelBase, IDataErrorInfo
{
private readonly TextBoxValidationModel _textbxValModel;
private Dictionary<string, bool> validProperties;
private bool allPropertiesValid = false;
private DelegateCommand exitCommand;
private DelegateCommand saveCommand;
public TextBoxValidationViewModel(TextBoxValidationModel newTextBoxValObj)
{
this._textbxValModel = newTextBoxValObj;
this.validProperties = new Dictionary<string, bool>();
this.validProperties.Add("BuyTh", false);
this.validProperties.Add("SellTh", false);
}
public string BuyTh
{
get { return _textbxValModel.BuyTh; }
set
{
if (_textbxValModel.BuyTh != value)
{
_textbxValModel.BuyTh = value;
base.OnPropertyChanged("BuyTh");
}
}
}
public string SellTh
{
get { return _textbxValModel.SellTh; }
set
{
if (_textbxValModel.SellTh != value)
{
_textbxValModel.SellTh = value;
base.OnPropertyChanged("SellTh");
}
}
}
public bool AllPropertiesValid
{
get { return allPropertiesValid; }
set
{
if (allPropertiesValid != value)
{
allPropertiesValid = value;
base.OnPropertyChanged("AllPropertiesValid");
}
}
}
public string this[string propertyName]
{
get
{
string error = (_textbxValModel as IDataErrorInfo)[propertyName];
validProperties[propertyName] = String.IsNullOrEmpty(error) ? true : false;
ValidateProperties();
CommandManager.InvalidateRequerySuggested();
return error;
}
}
public string Error
{
get
{
return (_textbxValModel as IDataErrorInfo).Error;
}
}
public ICommand ExitCommand
{
get
{
if (exitCommand == null)
{
exitCommand = new DelegateCommand(Exit);
}
return exitCommand;
}
}
public ICommand SaveCommand
{
get
{
if (saveCommand == null)
{
saveCommand = new DelegateCommand(Save);
}
return saveCommand;
}
}
#region private helpers
private void ValidateProperties()
{
foreach (bool isValid in validProperties.Values)
{
if (!isValid)
{
this.AllPropertiesValid = false;
return;
}
}
this.AllPropertiesValid = true;
}
private void Exit()
{
Application.Current.Shutdown();
}
private void Save()
{
_textbxValModel.Save();
}
}
}
#endregion
Model :
class TextBoxValidationModel : IDataErrorInfo
{
public string BuyTh { get; set; }
public string SellTh { get; set; }
public void Save()
{
//Insert code to save new Product to database etc
}
public string this[string propertyName]
{
get
{
string validationResult = null;
switch (propertyName)
{
case "BuyTh":
validationResult = ValidateName();
break;
case "SellTh":
validationResult = ValidateName();
break;
default:
throw new ApplicationException("Unknown Property being validated on Product.");
}
return validationResult;
}
}
public string Error
{
get
{
throw new NotImplementedException();
}
}
private string ValidateName()
{
return "Entered in validation Function";
}
}
}
ViewModelBase abstract Class :
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Application Start event code:
private void Application_Startup(object sender, StartupEventArgs e)
{
textboxvalwpf.Model.TextBoxValidationModel newTextBoxValObj = new Model.TextBoxValidationModel();
TextBoxValidation _txtBoxValView = new TextBoxValidation();
_txtBoxValView.DataContext = new textboxvalwpf.ViewModel.TextBoxValidationViewModel(newTextBoxValObj);
// _txtBoxValView.Show();
}
}
View Xaml code:
<Window x:Class="textboxvalwpf.TextBoxValidation"
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:c="clr-namespace:textboxvalwpf.Commands"
xmlns:local="clr-namespace:textboxvalwpf"
mc:Ignorable="d"
Title="TextBoxValidation" Height="300" Width="300">
<Grid>
<TextBox x:Name="textBox" HorizontalAlignment="Left" Height="23" Margin="86,44,0,0" TextWrapping="Wrap" Text="{Binding Path=BuyTh, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>
<TextBox x:Name="textBox1" HorizontalAlignment="Left" Height="23" Margin="88,121,0,0" TextWrapping="Wrap" Text="{Binding Path=SellTh,ValidatesOnDataErrors=True,UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>
<Label x:Name="label_BuyTh" Content="Buy Th" HorizontalAlignment="Left" Margin="10,44,0,0" VerticalAlignment="Top" Width="71"/>
<Label x:Name="label_SellTh" Content="Sell Th" HorizontalAlignment="Left" Margin="10,117,0,0" VerticalAlignment="Top" Width="71"/>
</Grid>
</Window>
In App.xaml, you'll find a StartupUri attribute. By default, it looks like this:
<Application x:Class="WPFTest.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WPFTest"
StartupUri="MainWindow.xaml">
If you renamed MainWindow.xaml to TextBoxValidation.xaml, as I think you did, it'll look like this:
StartupUri="TextBoxValidation.xaml">
Your application will automatically create an instance of that StartupUri window and show it. That'll be the application's main window, so the application will shut down when it closes.
That instance won't have a viewmodel, because nothing in your code gives it one. You are creating your own instance of the window, giving it a viewmodel, and then doing nothing with it because another instance is being shown. I imagine you thought just creating the window was enough to show it, but that's not what's happening at all. Before you commented out the Show() call, you did have one window with a working viewmodel that did the validation and updated the viewmodel properties.
Let the App create the window the way it wants to.
A quick, simple fix is to delete that Startup event handler from App and move your viewmodel creation code into TextBoxValidation's constructor:
public TextBoxValidation()
{
InitializeComponent();
textboxvalwpf.Model.TextBoxValidationModel newTextBoxValObj = new Model.TextBoxValidationModel();
this.DataContext = new textboxvalwpf.ViewModel.TextBoxValidationViewModel(newTextBoxValObj);
}
I'm trying to make a control that has a current value with an optional equation string.
I have 2 textboxes:
One (a) where you can enter an equation shortcut to a value to put into the other (b).
(b) contains the actual value.
(for example, in (a), if you enter 'pi', the second will then fill with "3.1415926535897931")
I'm using 2 textboxes so the user can refine their equation if they need to, and watch the value change as they modify it.
The data has 2 fields, one being the equation string and the other being the current value.
so I have (a).Text bound to the string, a new property on (a) that holds the value, and I bind (b).Text to the value also.
(a).Text is TwoWay
(a).Value is OneWayToSource (since changes to the text should only be pushed to b)
(b).Value is TwoWay
This all works fine if I have the data set in the constructor before any XAML binding, but does not work at all if I add the data after binding.
Here is a minimal amount of code that shows the problem.
The only comment is at the line that can make it work or not.
As a last resort I could turn it into a custom control and handle it in the code-behind, but I'd think this should work in the first place.
Any ideas why this isn't working?
Thanks!
Here is the XAML:
<Window x:Class="twoBindingsOnSameField.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:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:twoBindingsOnSameField"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Button Content="load data" Click="Button_Click" Width="80" IsEnabled="{Binding NeedsData}"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="enter text:" Width="80"/>
<local:TextBoxCalc Text="{Binding Item.ItemString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextBoxCalculatedValue="{Binding Item.ItemValue, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"
Width="200"
IsEnabled="{Binding HasData}"
/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="updated text:" Width="80"/>
<TextBox Text="{Binding Item.ItemValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Width="200"
IsEnabled="{Binding HasData}"
/>
</StackPanel>
</StackPanel>
</Window>
Here is the codebehind.
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace twoBindingsOnSameField
{
public partial class MainWindow : Window
{
data data;
public MainWindow()
{
InitializeComponent();
data = new data();
/// ---- Does not work with the following line commented out, but does if it is uncommented ----
/// ---- use the button to set the data ----
//setdata();
DataContext = data;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
setdata();
}
void setdata()
{
if (data.Item == null)
data.Item = new dataitem();
}
}
public class data : notifybase
{
dataitem item;
public data()
{
}
public dataitem Item
{
get
{
return item;
}
set
{
if (item != value)
{
item = value;
notifyPropertyChanged("Item");
notifyPropertyChanged("HasData");
notifyPropertyChanged("NeedsData");
}
}
}
public bool HasData
{
get
{
return Item != null;
}
}
public bool NeedsData
{
get
{
return Item == null;
}
}
}
public class dataitem : notifybase
{
string itemString;
string itemValue;
public dataitem()
{
itemString = "3";
itemValue = "4";
}
public virtual string ItemString
{
get
{
return this.itemString;
}
set
{
if (!object.Equals(this.itemString, value))
{
this.itemString = value;
notifyPropertyChanged("ItemString");
}
}
}
public virtual string ItemValue
{
get
{
return this.itemValue;
}
set
{
if (!object.Equals(this.itemValue, value))
{
this.itemValue = value;
notifyPropertyChanged("ItemValue");
}
}
}
}
public class TextBoxCalc : TextBox
{
public TextBoxCalc()
{
TextProperty.AddHandler(this, (o,e)=>TextBoxCalculatedValue="updated:" + Text);
}
#region TextBoxCalculatedValue
public static DependencyProperty TextBoxCalculatedValueProperty = DependencyProperty.Register("TextBoxCalculatedValue", typeof(string), typeof(TextBoxCalc), new PropertyMetadata(""));
public string TextBoxCalculatedValue
{
get
{
return (string)GetValue(TextBoxCalculatedValueProperty);
}
set
{
if (!object.Equals(TextBoxCalculatedValue, value))
SetValue(TextBoxCalculatedValueProperty, value);
}
}
#endregion
}
public class notifybase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
protected virtual void notifyPropertyChanged(string propertyName)
{
PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e);
}
}
static class extensions
{
public static void AddHandler(this DependencyProperty prop, object component, EventHandler handler)
{
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(prop, component.GetType());
if (dpd != null)
dpd.AddValueChanged(component, handler);
}
}
}
The reason why it works when you uncomment //setdata(); is because it is initializing the object in what is effectively your viewmodel, therefore you can change its properties via binding. To clarify as a side note, data would be your view model, and dataitem is your model, however you're dataitem is using INPC, so it doesn't really make sense in this case to have a viewmodel necessarily.
Anyways, the issue is that TextBoxCalculatedValue is set to a OneWayToSource binding. When you run the code commented out, its going to try and bind to a null value. When it does, it tries to update a null value, which isn't possible. WPF handles what would normally be a null exception automatically. When you update the dataItem by clicking the button, it doesn't update the object TextBoxCalc is bound to, so instead, it will continue trying to bind & update the null object. Change it to a TwoWay binding and you'll see a difference. Changing to TwoWay is probably your best option.
Good practice is to use constructor injection to practice dependency injection. With that being said, passing a dataItem to data would be the best route, and at the very least, initializing dataItem in data's constructor would be an ideal approach. So,
public data(dataItem item)
{
Item = item;
}
or
public data()
{
Item = new dataitem();
}
I have been looking at MVVM for the last couple days and thought i would try out a simple example to update a text box with a time. However i'm having a bit of trouble wrapping my head around this. I have a two projects in my Solution.. one i'm calling TimeProvider that right now is just returning Datetime event and another that is called E. Eventually i will use TimeProvider to provide a lot more information but i want to understand something simple first. Can some one please tell me why i'm not geting the gui to update.
namespace E.TimeProvider
{
public interface ITimeSource
{
void Subscribe();
event Action<Time> TimeArrived;
}
}
namespace E.TimeProvider
{
public class Time
{
private DateTime _earthDate;
public Time()
{
}
public Time(DateTime earthDate)
{
this._earthDate = earthDate;
}
public DateTime EarthDate
{
get { return _earthDate; }
set { _earthDate = value; }
}
}
}
namespace E.TimeProvider
{
public class TimeSource : ITimeSource
{
private const int TIMER_INTERVAL = 50;
public event Action<Time> TimeArrived;
private bool subscribe;
public TimeSource()
{
subscribe = false;
Thread timeGenerator = new Thread(new ThreadStart(GenerateTimes));
timeGenerator.IsBackground = true;
timeGenerator.Priority = ThreadPriority.Normal;
timeGenerator.Start();
}
public void Subscribe()
{
if (subscribe)
return;
subscribe = true;
}
private void GenerateTimes()
{
while (true)
{
GenerateAndPublishTimes();
Thread.Sleep(TIMER_INTERVAL);
}
}
private void GenerateAndPublishTimes()
{
DateTime earthDate = DateTime.Now;
Time time = new Time(earthDate);
TimeArrived(time);
}
}
}
Then i have my project E
xaml
<Window x:Class="E.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="200" xmlns:my="clr-namespace:Exiled" WindowStyle="None" WindowStartupLocation="CenterScreen" ResizeMode="NoResize">
<Grid>
<my:TimeControl HorizontalAlignment="Left" x:Name="timeControl1" VerticalAlignment="Top" Height="300" Width="200" />
</Grid>
</Window>
<UserControl x:Class="E.TimeControl"
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="300" d:DesignWidth="200" Background="Black" Foreground="White">
<Grid>
<TextBlock Height="41" HorizontalAlignment="Left" Margin="12,31,0,0" Text="{Binding Path=EarthTime}" VerticalAlignment="Top" Width="176" FontSize="35" TextAlignment="Center" />
</Grid>
</UserControl>
and the rest
namespace E
{
public class TimeControlViewModel : DependencyObject
{
private readonly ITimeSource _source;
public ObservableCollection<TimeViewModel> Times { get; set; }
public TimeControlViewModel()
{
this.Times = new ObservableCollection<TimeViewModel>();
}
public TimeControlViewModel(ITimeSource source)
{
this.Times = new ObservableCollection<TimeViewModel>();
_source = source;
_source.TimeArrived += new Action<Time>(_source_TimeArrived);
}
public void Subscribe()
{
_source.Subscribe();
}
void _source_TimeArrived(Time time)
{
TimeViewModel tvm = new TimeViewModel();
tvm.Time = time;
}
}
}
namespace E
{
class SubscribeCommand
{
private readonly TimeControlViewModel _vm;
public SubscribeCommand(TimeControlViewModel vm)
{
_vm = vm;
}
public void Execute(object parameter)
{
_vm.Subscribe();
}
}
}
namespace E
{
public class TimeViewModel : DependencyObject
{
public TimeViewModel()
{
}
public Time Time
{
set
{
this.EarthDate = value.EarthDate;
}
}
public DateTime EarthDate
{
get { return (DateTime)GetValue(DateProperty); }
set { SetValue(DateProperty, value); }
}
// Using a DependencyProperty as the backing store for Date. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DateProperty =
DependencyProperty.Register("EarthDate", typeof(DateTime), typeof(TimeViewModel), new UIPropertyMetadata(DateTime.Now));
}
}
You've got a few issues here:
You're not seeing an update, because the DataContext of your Window or UserControl is not set to the TimeViewModel instance you create.
Typically, ViewModel instances should not be DependencyObjects. Instead, implement INotifyPropertyChanged. This keeps them from having a dependency on WPF, but still allows them to work properly with MVVM.
I'd recommend reading through a detailed introduction to MVVM, such as the one I wrote here. In particular, you'll need to understand how templating works and the DataContext in order to use binding properly.
For a start it looks like you're binding to EarthTime:
Text="{Binding Path=EarthTime}"
but the property itself is called EarthDate
public DateTime EarthDate
I'm not sure my Title is right but this is the problem I am facing now.. I have the below XAML code..
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding Path=AvailableFields}"
SelectedItem="{Binding Path=SelectedField}"
></ComboBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
What this basically does is, If my data source contains ten items, this is going to generate 10 row of comboboxes and all comboboxes are bounded to the same itemsource.
Now my requirement is Once an item is selected in the first combo box, that item should not be available in the subsequent combo boxes. How to satisfy this requirement in MVVM and WPF?
This turned out to be harder than I thought when I started coding it. Below sample does what you want. The comboboxes will contain all letters that are still available and not selected in another combobox.
XAML:
<Window x:Class="TestApp.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<StackPanel>
<ItemsControl ItemsSource="{Binding Path=SelectedLetters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox
ItemsSource="{Binding Path=AvailableLetters}"
SelectedItem="{Binding Path=Letter}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Window>
Code behind:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace TestApp
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new VM();
}
}
public class VM : INotifyPropertyChanged
{
public VM()
{
SelectedLetters = new List<LetterItem>();
for (int i = 0; i < 10; i++)
{
LetterItem letterItem = new LetterItem();
letterItem.PropertyChanged += OnLetterItemPropertyChanged;
SelectedLetters.Add(letterItem);
}
}
public List<LetterItem> SelectedLetters { get; private set; }
private void OnLetterItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != "Letter")
{
return;
}
foreach (LetterItem letterItem in SelectedLetters)
{
letterItem.RefreshAvailableLetters(SelectedLetters);
}
}
public event PropertyChangedEventHandler PropertyChanged;
public class LetterItem : INotifyPropertyChanged
{
static LetterItem()
{
_allLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".Select(c => c.ToString());
}
public LetterItem()
{
AvailableLetters = _allLetters;
}
public void RefreshAvailableLetters(IEnumerable<LetterItem> letterItems)
{
AvailableLetters = _allLetters.Where(c => !letterItems.Any(li => li.Letter == c) || c == Letter);
}
private IEnumerable<string> _availableLetters;
public IEnumerable<string> AvailableLetters
{
get { return _availableLetters; }
private set
{
_availableLetters = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("AvailableLetters"));
}
}
}
private string _letter;
public string Letter
{
get { return _letter; }
set
{
if (_letter == value)
{
return;
}
_letter = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Letter"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private static readonly IEnumerable<string> _allLetters;
}
}
}
This functionality is not provided by WPF, but it can be implemented using some custom coding.
I've created 3 ViewModel classes:
PreferencesVM - This will be our DataContext. It contains the master list of options which can appear in the ComboBoxes, and also contains a SelectedOptions property, which keeps track of which items are selected in the various ComboBoxes. It also has a Preferences property, which we will bind our ItemsControl.ItemsSource to.
PreferenceVM - This represents one ComboBox. It has a SelectedOption property, which ComboBox.SelectedItem is bound to. It also has a reference to PreferencesVM, and a property named Options (ComboBox.ItemsSource is bound to this), which returns the Options on PreferencesVM via a filter which checks if the item may be displayed in the ComboBox.
OptionVM - Represents a row in the ComboBox.
The following points form the key to the solution:
When PreferenceVM.SelectedOption is set (ie a ComboBoxItem is selected), the item is added to the PreferencesVM.AllOptions collection.
PreferenceVM handles Preferences.SelectedItems.CollectionChanged, and triggers a refresh by raising PropertyChanged for the Options property.
PreferenceVM.Options uses a filter to decide which items to return - which only allows items which are not in PreferencesVM.SelectedOptions, unless they are the SelectedOption.
What I've described above might be enough to get you going, but to save you the headache I'll post my code below.
PreferencesVM.cs:
public class PreferencesVM
{
public PreferencesVM()
{
PreferenceVM pref1 = new PreferenceVM(this);
PreferenceVM pref2 = new PreferenceVM(this);
PreferenceVM pref3 = new PreferenceVM(this);
this._preferences.Add(pref1);
this._preferences.Add(pref2);
this._preferences.Add(pref3);
//Only three ComboBoxes, but you can add more here.
OptionVM optRed = new OptionVM("Red");
OptionVM optGreen = new OptionVM("Green");
OptionVM optBlue = new OptionVM("Blue");
_allOptions.Add(optRed);
_allOptions.Add(optGreen);
_allOptions.Add(optBlue);
}
private ObservableCollection<OptionVM> _selectedOptions =new ObservableCollection<OptionVM>();
public ObservableCollection<OptionVM> SelectedOptions
{
get { return _selectedOptions; }
}
private ObservableCollection<OptionVM> _allOptions = new ObservableCollection<OptionVM>();
public ObservableCollection<OptionVM> AllOptions
{
get { return _allOptions; }
}
private ObservableCollection<PreferenceVM> _preferences = new ObservableCollection<PreferenceVM>();
public ObservableCollection<PreferenceVM> Preferences
{
get { return _preferences; }
}
}
PreferenceVM.cs:
public class PreferenceVM:INotifyPropertyChanged
{
private PreferencesVM _preferencesVM;
public PreferenceVM(PreferencesVM preferencesVM)
{
_preferencesVM = preferencesVM;
_preferencesVM.SelectedOptions.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedOptions_CollectionChanged);
}
void SelectedOptions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this,new PropertyChangedEventArgs("Options"));
}
private OptionVM _selectedOption;
public OptionVM SelectedOption
{
get { return _selectedOption; }
set
{
if (value == _selectedOption)
return;
if (_selectedOption != null)
_preferencesVM.SelectedOptions.Remove(_selectedOption);
_selectedOption = value;
if (_selectedOption != null)
_preferencesVM.SelectedOptions.Add(_selectedOption);
}
}
private ObservableCollection<OptionVM> _options = new ObservableCollection<OptionVM>();
public IEnumerable<OptionVM> Options
{
get { return _preferencesVM.AllOptions.Where(x=>Filter(x)); }
}
private bool Filter(OptionVM optVM)
{
if(optVM==_selectedOption)
return true;
if(_preferencesVM.SelectedOptions.Contains(optVM))
return false;
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
}
OptionVM.cs:
public class OptionVM
{
private string _name;
public string Name
{
get { return _name; }
}
public OptionVM(string name)
{
_name = name;
}
}
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new PreferencesVM();
}
}
MainWindow.xaml:
<Window x:Class="WpfApplication64.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>
<ItemsControl ItemsSource="{Binding Path=Preferences}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path=Options}" DisplayMemberPath="Name" SelectedItem="{Binding Path=SelectedOption}"></ComboBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
**Note that to reduce lines of code, my provided solution only generates 3 ComboBoxes (not 10).
I have a Silverlight page that gets its data from a view model class which aggregates some data from various (RIA services) domain services.
Ideally I'd like the page to be able to data bind its controls to properties of the view model object, but because DomainContext.Load executes a query asynchronously, the data is not available when the page loads.
My Silverlight page has the following XAML:
<navigation:Page x:Class="Demo.UI.Pages.WidgetPage"
// the usual xmlns stuff here...
xmlns:local="clr-namespace:Demo.UI.Pages" mc:Ignorable="d"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
d:DataContext="{d:DesignInstance Type=local:WidgetPageModel, IsDesignTimeCreatable=False}"
d:DesignWidth="640" d:DesignHeight="480"
Title="Widget Page">
<Canvas x:Name="LayoutRoot">
<ListBox ItemsSource="{Binding RedWidgets}" Width="150" Height="500" />
</Canvas>
</navigation:Page>
My ViewModel looks like this:
public class WidgetPageModel
{
private WidgetDomainContext WidgetContext { get; set; }
public WidgetPageModel()
{
this.WidgetContext = new WidgetDomainContext();
WidgetContext.Load(WidgetContext.GetAllWidgetsQuery(), false);
}
public IEnumerable<Widget> RedWidgets
{
get
{
return this.WidgetContext.Widgets.Where(w => w.Colour == "Red");
}
}
}
I think this approach must be fundamentally wrong because the asynchronous nature of Load means that the widget list is not necessarily populated when the ListBox data binds. (A breakpoint in my repository shows that the code to populate to collection is being executed, but only after the page renders.)
Can someone please show me the right way to do this?
The missing piece of the puzzle is that I needed to be raising events when the properties changed.
My updated ViewModel is as follows:
public class WidgetPageModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private WidgetDomainContext WidgetContext { get; set; }
public WidgetPageModel()
{
this.WidgetContext = new WidgetDomainContext();
WidgetContext.Load(WidgetContext.GetAllWidgetsQuery(),
(result) =>
{
this.RedWidgets = this.WidgetContext.Widgets.Where(w => w.Colour == "Red");
}, null);
}
private IEnumerable<Widget> _redWidgets;
public IEnumerable<Widget> RedWidgets
{
get
{
return _redWidgets;
}
set
{
if(value != _redWidgets)
{
_redWidgets = value;
RaisePropertyChanged("RedWidgets");
}
}
}
}
The controls bound to these properties are updated when the property change event fires.