I'm using MVVM, VS 2008, and .NET 3.5 SP1. I have a list of items, each exposing an IsSelected property. I have added a CheckBox to manage the selection/de-selection of all the items in the list (updating each item's IsSelected property). Everything is working except the IsChecked property is not being updated in the view when the PropertyChanged event fires for the CheckBox's bound control.
<CheckBox
Command="{Binding SelectAllCommand}"
IsChecked="{Binding Path=AreAllSelected, Mode=OneWay}"
Content="Select/deselect all identified duplicates"
IsThreeState="True" />
My VM:
public class MainViewModel : BaseViewModel
{
public MainViewModel(ListViewModel listVM)
{
ListVM = listVM;
ListVM.PropertyChanged += OnListVmChanged;
}
public ListViewModel ListVM { get; private set; }
public ICommand SelectAllCommand { get { return ListVM.SelectAllCommand; } }
public bool? AreAllSelected
{
get
{
if (ListVM == null)
return false;
return ListVM.AreAllSelected;
}
}
private void OnListVmChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "AreAllSelected")
OnPropertyChanged("AreAllSelected");
}
}
I'm not showing the implementation of SelectAllCommand or individual item selection here, but it doesn't seem to be relevant. When the user selects a single item in the list (or clicks the problem CheckBox to select/de-select all items), I have verified that the OnPropertyChanged("AreAllSelected") line of code executes, and tracing in the debugger, can see the PropertyChanged event is subscribed to and does fire as expected. But the AreAllSelected property's get is only executed once - when the view is actually rendered. Visual Studio's Output window does not report any data binding errors, so from what I can tell, the CheckBox's IsSelected property is properly bound.
If I replace the CheckBox with a Button:
<Button Content="{Binding SelectAllText}" Command="{Binding SelectAllCommand}"/>
and update the VM:
...
public string SelectAllText
{
get
{
var msg = "Select All";
if (ListVM != null && ListVM.AreAllSelected != null && ListVM.AreAllSelected.Value)
msg = "Deselect All";
return msg;
}
}
...
private void OnListVmChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "AreAllSelected")
OnPropertyChanged("SelectAllText");
}
everything works as expected - the button's text is updated as all items are selected/desected. Is there something I'm missing about the Binding on the CheckBox's IsSelected property?
Thanks for any help!
I found the problem. It seems a bug existed in WPF 3.0 with OneWay bindings on IsChecked causing the binding to be removed. Thanks to this post for the assistance, it sounds like the bug was fixed in WPF 4.0
To reproduce, create a new WPF project.
Add a FooViewModel.cs:
using System;
using System.ComponentModel;
using System.Windows.Input;
namespace Foo
{
public class FooViewModel : INotifyPropertyChanged
{
private bool? _isCheckedState = true;
public FooViewModel()
{
ChangeStateCommand = new MyCmd(ChangeState);
}
public bool? IsCheckedState
{
get { return _isCheckedState; }
}
public ICommand ChangeStateCommand { get; private set; }
private void ChangeState()
{
switch (_isCheckedState)
{
case null:
_isCheckedState = true;
break;
default:
_isCheckedState = null;
break;
}
OnPropertyChanged("IsCheckedState");
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var changed = PropertyChanged;
if (changed != null)
changed(this, new PropertyChangedEventArgs(propertyName));
}
}
public class MyCmd : ICommand
{
private readonly Action _execute;
public event EventHandler CanExecuteChanged;
public MyCmd(Action execute)
{
_execute = execute;
}
public void Execute(object parameter)
{
_execute();
}
public bool CanExecute(object parameter)
{
return true;
}
}
}
Modify Window1.xaml.cs:
using System.Windows;
using System.Windows.Controls.Primitives;
namespace Foo
{
public partial class Window1
{
public Window1()
{
InitializeComponent();
}
private void OnClick(object sender, RoutedEventArgs e)
{
var bindingExpression = MyCheckBox.GetBindingExpression(ToggleButton.IsCheckedProperty);
if (bindingExpression == null)
MessageBox.Show("IsChecked property is not bound!");
}
}
}
Modify Window1.xaml:
<Window
x:Class="Foo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Foo"
Title="Window1"
Height="200"
Width="200"
>
<Window.DataContext>
<vm:FooViewModel />
</Window.DataContext>
<StackPanel>
<CheckBox
x:Name="MyCheckBox"
Command="{Binding ChangeStateCommand}"
IsChecked="{Binding Path=IsCheckedState, Mode=OneWay}"
Content="Foo"
IsThreeState="True"
Click="OnClick"/>
<Button Command="{Binding ChangeStateCommand}" Click="OnClick" Content="Change State"/>
</StackPanel>
</Window>
Click on the button a few times and see the CheckBox's state toggle between true and null (not false). But click on the CheckBox and you will see that the Binding is removed from the IsChecked property.
The workaround:
Update the IsChecked binding to be TwoWay and set its UpdateSourceTrigger to be explicit:
IsChecked="{Binding Path=IsCheckedState, Mode=TwoWay, UpdateSourceTrigger=Explicit}"
and update the bound property so it's no longer read-only:
public bool? IsCheckedState
{
get { return _isCheckedState; }
set { }
}
Related
I've seen this question posted (and answered) a number of times, and I still can't seem to figure out what I'm missing...
I have a window with a list of checkboxes, and I want the ability to have checkboxes in the list enabled/disabled dynamically from code-behind. To do that I've got couple of radio buttons that call a code-behind function to toggle the 'Enabled' property of the first entry in the VisibleFeatures collection. Ideally, this would cause the first checkbox + text to enable/disable, but no UI changes occur.
What am I doing wrong?
ViewModel:
public class MyFeature
{
private bool _supported;
private bool _enabled;
private bool _selected;
public string Name { get; set; }
public bool Supported
{
get { return _supported; }
set { _supported = value; NotifyPropertyChanged("Supported"); }
}
public bool Enabled
{
get { return _enabled; }
set { _visible = value; NotifyPropertyChanged("Enabled"); }
}
public bool Selected
{
get { return _selected; }
set { _selected = value; NotifyPropertyChanged("Selected"); }
}
public MyFeature(string name)
{
Name = name;
_supported = false;
_enabled = false;
_selected = false;
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public ObservableCollection<MyFeature> VisibleFeatures { get; set; }
void VisibleFeatures_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach (MyFeature item in e.NewItems)
item.PropertyChanged += MyFeature_PropertyChanged;
if (e.OldItems != null)
foreach (MyFeature item in e.OldItems)
item.PropertyChanged -= MyFeature_PropertyChanged;
}
void MyFeature_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// NotifyPropertyChanged() defined again elsewhere in the class
NotifyPropertyChanged("VisibleFeatures");
}
public Init()
{
VisibleFeatures = new ObservableCollection<MyFeature>();
VisibleFeatures.CollectionChanged += VisibleFeatures_CollectionChanged;
VisibleFeatures.Add(new MyFeature("Feature1"));
VisibleFeatures.Add(new MyFeature("Feature2"));
...
}
XAML:
<StackPanel>
<ListView ItemsSource="{Binding VisibleFeatures}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel IsEnabled="{Binding Enabled, Mode=TwoWay}">
<CheckBox IsChecked="{Binding Selected, Mode=TwoWay}">
<TextBlock Text="{Binding Name}" />
</CheckBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListView>
</StackPanel>
Your class MyFeature needs to declare that it implements interface INotifyPropertyChanged. Otherwise, there will be no listener generated from XAML to listen to your property change notification.
Beside, from your example, I see no use of notifying VisibleFeatures change.
Derive your class "MyFeature" from INotifyPropertyChanged interface.
Inorder to reflect your runtime changes made in your observable collection in view, it is mandatory to derive your viewmodel class (here MyFeature class) from INotifyPropertyChanged interface.
Also, it is advisable to use same instance of your binding property wherever it is used instead of creating a new instance.
Question
How can I make it so that changes to a note are only propagated back to the list, when the Save button is clicked instead on "lost focus"?
And the Save button should only be enabled when the note has been changed.
UI
The example application looks like this:
The current behaviour is:
Clicking on a note puts its text into the TextBox; that's fine.
The changed text from the TextBox gets written back to the list when the TextBox loses the focus (default binding behaviour); but I only want that to happend when the Save button is clicked.
The Save button is always activated because the CanExecute(object parameter) isn't correctly implemented yet; it should only get activated when the TextBox text is different from the selected note's text.
My research so far
Option 1: Some Internet sources say to bind a different property to the TextBox and to programmatically check whether it is different from the SelectedItem of the ListView. I would have hoped that there was a way without introducing a third property in addition to the already existing ListOfNotes and SelectedNote.
Option 2: Some Internet sources recommend to configure Mode=OneWay so that clicking an item in the ListView updates the TextBox, but not the other way around. This sounds like the solution I would prefer, but I wasn't able to figure out from the code examples how to raise an event programmatically so that the change in the TextBox gets written back to the ListView when the Save button is clicked.
I've found other Stackoverflow questions that seem to be similar to mine, but the answers to those haven't helped me fix the problem:
WPF databinding after Save button click
Code
This example currently does two-way binding on focus lost. How do I need to change it to get the above described behaviour?
https://github.com/lernkurve/WpfBindingOneWayWithSaveButton
MainWindow.xaml
<Window x:Class="WpfBindingOneWayWithSaveButton.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:wpfBindingOneWayWithSaveButton="clr-namespace:WpfBindingOneWayWithSaveButton"
mc:Ignorable="d"
Title="MainWindow" Height="188.636" Width="299.242">
<Window.DataContext>
<wpfBindingOneWayWithSaveButton:MainWindowsViewModel />
</Window.DataContext>
<Grid>
<GroupBox Header="List of notes" HorizontalAlignment="Left" VerticalAlignment="Top" Height="112" Width="129" Margin="0,24,0,0">
<ListView ItemsSource="{Binding ListOfNotes}" SelectedItem="{Binding SelectedNote}" DisplayMemberPath="Text" HorizontalAlignment="Left" Height="79" VerticalAlignment="Top" Width="119" Margin="0,10,-2,0"/>
</GroupBox>
<GroupBox Header="Change selected note" HorizontalAlignment="Left" Margin="134,24,0,0" VerticalAlignment="Top" Height="112" Width="151">
<Grid HorizontalAlignment="Left" Height="89" Margin="0,0,-2,0" VerticalAlignment="Top" Width="141">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40*"/>
<ColumnDefinition Width="101*"/>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding SelectedNote.Text}" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" VerticalAlignment="Top" Width="121" Margin="10,7,0,0" Grid.ColumnSpan="2"/>
<Button Command="{Binding SaveCommand}" Content="Save" HorizontalAlignment="Left" VerticalAlignment="Top" Width="121" Margin="10,35,0,0" Grid.ColumnSpan="2"/>
</Grid>
</GroupBox>
</Grid>
</Window>
MainWindowsViewModel.cs
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace WpfBindingOneWayWithSaveButton
{
public class MainWindowsViewModel
{
public ObservableCollection<Note> ListOfNotes { get; set; }
public Note SelectedNote { get; set; }
public ICommand SaveCommand { get; set; }
public MainWindowsViewModel()
{
ListOfNotes = new ObservableCollection<Note>
{
new Note { Text = "Note 1" },
new Note { Text = "Note 2" }
};
SaveCommand = new SaveCommand(this);
}
}
}
SaveCommand.cs
using System;
using System.Windows.Input;
namespace WpfBindingOneWayWithSaveButton
{
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
// What should go here?
return true;
// Pseudo code
// return (is the TextBox text different from the original note text)
}
public void Execute(object parameter)
{
// What should go here?
// Pseudo code
// Let WPF know that the TextBox text has changed
// Invoke the binding so it propagates the TextBox text back to the list
}
public event EventHandler CanExecuteChanged;
}
}
Note.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfBindingOneWayWithSaveButton
{
public class Note : INotifyPropertyChanged
{
private string text;
public string Text
{
get { return text; }
set
{
text = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Bind the text to the CommandParameter of the SaveButton so it gets passed to the Save method for updating.
<TextBox x:Name="NoteTextBox" Text="{Binding SelectedNote.Text, Mode=OneTime}" ../>
<Button Command="{Binding SaveCommand}"
CommandParameter="{Binding ElementName=NoteTextBox, Path=Text}",
Content="Save" />
and
public bool CanExecute(object parameter)
{
return vm.SelectedNote.Text != parameter as string;
}
public void Execute(object parameter)
{
vm.SelectedNote.Text = parameter as string;
}
Option one is the easiest to implement, you will need to clone the Note object and set it to a separate property.
in your xaml, change your list view to the following so it now binds the SelectedIndex instead of the SelectedItem.
<ListView ItemsSource="{Binding ListOfNotes}" SelectedIndex="{Binding SelectedIndex}" DisplayMemberPath="Text" ...
And change TextBox to the following so it updates the binding as you type
<TextBox Text="{Binding Path=SelectedNote.Text, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" ...
In Note.cs we add the Clone() method.
public class Note : INotifyPropertyChanged
{
public Note Clone()
{
return new Note()
{
Text = this.Text
};
}
//... The rest stays the same
}
In MainWindowsViewModel.cs we add new properties for the SelectedIndex and clone the object when we detect a index has changed. We also need to add INotifyPropertyChanged so we can update the SelectedNote from the codebehind when we do the Clone()
public class MainWindowsViewModel : INotifyPropertyChanged
{
private int _selectedIndex = -1;
private Note _selectedNote;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex.Equals(value))
return;
_selectedIndex = value;
CloneSelectedNote();
}
}
private void CloneSelectedNote()
{
if (SelectedIndex >= 0)
{
SelectedNote = ListOfNotes[SelectedIndex].Clone();
}
else
{
SelectedNote = null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Note SelectedNote
{
get { return _selectedNote; }
set
{
if(Equals(_selectedNote, value))
return;
_selectedNote = value;
OnPropertyChanged();
}
}
//... The rest stays the same
}
In SaveCommand.cs we add the logic for CanExecute and add the subscriptions to CommandManager.RequerySuggested, this automatically makes it requery the CanExecute any time any binding changes. This can be a little ineffecent, if you wanted to you could expose a RaiseCanExecuteChanged() publicly but it would be MainWindowsViewModel responsibility to call it any time vm.SelectedIndex or vm.SelectedNote.Text changed.
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
if (vm.SelectedIndex < 0 || vm.SelectedNote == null)
return false;
return vm.ListOfNotes[vm.SelectedIndex].Text != vm.SelectedNote.Text;
}
public void Execute(object parameter)
{
vm.ListOfNotes[vm.SelectedIndex] = vm.SelectedNote;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
UPDATE: Here is a updated version that does not use CommandManager
MainWindowsViewModel.cs
public class MainWindowsViewModel : INotifyPropertyChanged
{
private int _selectedIndex = -1;
private Note _selectedNote;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex.Equals(value))
return;
_selectedIndex = value;
CloneSelectedNote();
RecheckSaveCommand();
}
}
private void CloneSelectedNote()
{
if (SelectedIndex >= 0)
{
SelectedNote = ListOfNotes[SelectedIndex].Clone();
}
else
{
SelectedNote = null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Note SelectedNote
{
get { return _selectedNote; }
set
{
if(Equals(_selectedNote, value))
return;
if (_selectedNote != null)
{
PropertyChangedEventManager.RemoveHandler(_selectedNote, SelectedNoteTextChanged, nameof(Note.Text));
}
_selectedNote = value;
if (_selectedNote != null)
{
PropertyChangedEventManager.AddHandler(_selectedNote, SelectedNoteTextChanged, nameof(Note.Text));
}
OnPropertyChanged();
}
}
private void SelectedNoteTextChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
RecheckSaveCommand();
}
private void RecheckSaveCommand()
{
var command = this.SaveCommand as WpfBindingOneWayWithSaveButton.SaveCommand; //"this." and "WpfBindingOneWayWithSaveButton." are not necessary but I wanted to be explicit.
if (command != null)
{
command.RaiseCanExecuteChanged();
}
}
//...
}
SaveCommand.cs
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
if (vm.SelectedIndex < 0 || vm.SelectedNote == null)
return false;
return vm.ListOfNotes[vm.SelectedIndex].Text != vm.SelectedNote.Text;
}
public void Execute(object parameter)
{
vm.ListOfNotes[vm.SelectedIndex] = vm.SelectedNote;
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
You should not use OneWay but rather an UpdateSourceTrigger of value Explicit. BindingGroups can do this for you though, here's a simple example:
<!-- For change observation -->
<TextBlock Text="{Binding Text}"></TextBlock>
<StackPanel>
<StackPanel.BindingGroup>
<BindingGroup x:Name="EditGroup"></BindingGroup>
</StackPanel.BindingGroup>
<TextBox Text="{Binding Text}"></TextBox>
<Button>
<Button.Command>
<local:CommitGroupCommand BindingGroup="{x:Reference EditGroup}"/>
</Button.Command>
Save
</Button>
</StackPanel>
public class CommitGroupCommand : ICommand
{
public BindingGroup BindingGroup { get; set; }
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
BindingGroup.UpdateSources();
}
}
(You could add a validation rule to your binding that requires the value to be different and use that for the CanExecute implementation.)
Using this method allows you to bind directly to the object you intend to edit, so you don't need to copy around values first.
My WPF app is working in a strange way for me - some binding works, other not.
I have following situation:
A textbox - user provides an ID. Based on this ID an object is loaded or created. Some other properties are updated by values coming from the loaded/new object.
Binding for the ID textbox works fine. However, two other views (any other) not.
My code samples:
XAML:
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<TextBlock Text="ID" FontFamily="Segoe UI Light" />
<TextBox x:Name="TB_PacientID" Width="100px" HorizontalAlignment="Left" Margin="5,0,0,0" Text="{Binding Path=PacientID}"/>
<TextBlock x:Name="TBL_NovyPacient" Text="novĂ˝ pacient" Margin="5,0,0,0" Foreground="Green" FontWeight="Bold" Visibility="{Binding Path=IsNewPacient,UpdateSourceTrigger=PropertyChanged,Converter={StaticResource BTVConverter}}"/>
</StackPanel>
<WrapPanel x:Name="WP_PacientData" Margin="-2,5,2,5" Visibility="{Binding PacientLoaded,Converter={StaticResource BTVConverter}}">
...
Viewmodel:
public int? PacientID
{
get
{
if (CurrentPacient == null)
return null;
return CurrentPacient.id;
}
set
{
if (value != null)
{
_pacient = App.instance.sessionData.serviceProxy.pacientById(value.Value);
if (_pacient == null)
{
CurrentPacient = new Pacient() { id = value.Value };
IsNewPacient = true;
}
else
{
CurrentPacient = _pacient;
}
OnPropertyChanged();
PacientLoaded = true;
}
}
}
// ...
public bool IsNewPacient
{
get{ return _isNewPacient; }
set
{
_isNewPacient = value;
OnPropertyChanged();
}
}
//...
public bool PacientLoaded
{
get{ return _pacientLoaded; }
set
{
_pacientLoaded = value;
OnPropertyChanged();
}
}
The idea:
User inputs the ID, an object is loaded or created and the WrapPanel is shown. If the object is newly created the TextBlock is shown as well.
The converters are working fine (tested in another window).
When the window loads, the binding is established well (if I set some fake values in ctor). When changing the ID in textbox, nothing other updates - except for the ID itself - the setter is fired well and the new value is read after OnPropertyChanged is called.
I hope I'm missing something very easy and stupid.
-Edit:
Current state:
TB_PacientID is working (updading), TBL_NovyPacient and WP_PacientData not working (updating).
I want:
All thee views updating from viewmodel (the code properties).
-Edit 2
I created a very simple example of my problem from scratch:
A window - two textboxes:
<Window x:Class="bindingTest.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>
<TextBox x:Name="TestTextBox" Text="{Binding ID, Mode=TwoWay}"/>
<TextBox x:Name="SecondTextBox" Text="{Binding IsNew, Mode=TwoWay}"/>
</StackPanel>
</Window>
Codebehind:
namespace bindingTest
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new TestViewModel();
}
}
}
And the viewmodel class:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace bindingTest
{
public abstract class ViewModelBase
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class TestViewModel : ViewModelBase
{
private bool _attOne;
private int? id;
private bool _isNew;
public bool IsNew
{
get
{
return _isNew;
}
set
{
_isNew = value;
OnPropertyChanged();
}
}
public int? ID
{
get
{
return id;
}
set
{
this.id = value;
IsNew = true;
OnPropertyChanged();
}
}
}
}
And what I simply want - If I change the number in the first textbox I want to have True in the second textbox automatically.
Yes, I am stupid.
My ViewModel base class lost the INotifyPropertyChanged interface while merging from another project.
So I called the OnPropertyChanged, but it has been my own OnPropertyChanged instead of implementation of the interface which is WPF binding waiting for.
I had threethings to point in your code sample:
You should use a TwoWay binding for setting the ID.
Are you sure the _pacient = App.instance.sessionData.serviceProxy.pacientById(value.Value); code returns always the same object instance.
Are you correctly using the INotifyPropertyChanged interface in most cases you raising a property change events looks like this: RaisePropertyChanged('PropertyName'); you are invoking: 'OnPropertyChanged();'
Hope this helps...
I am trying to bind a ViewModel property of type Visibility to the visibility property on a Dock Panel:
Updated ViewModel Code:
public class SelectWaferButtonViewModel : INotifyPropertyChanged
{
private bool isClicked;
public SelectWaferButtonViewModel()
{
isClicked = false;
}
public bool IsControlVisible
{
get
{
return isClicked;
}
set
{
isClicked = value;
OnPropertyChanged("IsControlVisible");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnButtonClick()
{
if (isClicked)
{
IsControlVisible = false;
}
else
{
IsControlVisible = true;
}
}
protected virtual void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
and here is my updated XAML code:
<DockPanel
Name="tvwDockPanel"
Width="200"
Visibility="{Binding IsControlVisible, FallbackValue=Collapsed, Converter={StaticResource BoolToVisConverter}}"
DockPanel.Dock="Left">
<DockPanel
DockPanel.Dock="Top"
Height="22">
</DockPanel>
and I set the data context in the code behind with this line:
tvwDockPanel.DataContext = btnSelectWaferViewModel;
where btnSelectWaferViewModel is the ViewModel object for this situation.
and for fun, here is my code behind:
public partial class WaferTrackerWindow : Window
{
List<ISubscribeEvents> subscriptionList;
SelectWaferButtonViewModel btnSelectWaferViewModel;
public WaferTrackerWindow()
{
InitializeComponent();
this.InstantiateObjects();
this.SubscribeEvents();
this.SetDataContexts();
}
#region Methods
private void SetDataContexts()
{
tvwDockPanel.DataContext = btnSelectWaferViewModel.IsControlVisible;
}
private void SubscribeEvents()
{
foreach (ISubscribeEvents subscriber in subscriptionList)
{
subscriber.SubscribeEvents();
}
}
private void InstantiateObjects()
{
btnSelectWaferViewModel = new SelectWaferButtonViewModel();
subscriptionList = new List<ISubscribeEvents>();
subscriptionList.Add(
new Classes.WaferTrackerWindow.SelectWaferButtonView(btnSelectWafer, btnSelectWaferViewModel));
}
#endregion
}
All I want to do click the button btnSelectWafer and have the tvwDockPanel's visibility property to get to set to Visible via binding. Then when you click again on btnSelectWafer, tvwDockPanel's visibility property gets set back to Collapsed again. tvwDockPanel's visibility will only ever be either Collapsed or Visible.
Any help would be awesome, I am rather new to this whole data binding concept.
You have several issues here:
First of all, the intent of MVVM (if you're trying to do this with MVVM) is to separate logic from presentation. This means that in no way your ViewModel can have a reference to System.Windows.Controls.Button, nor to System.Windows.Visibility, nor to any other classes inside the System.Windows Namespace.
It is not clear to me what your SelectWaferButtonViewModel class is doing with the Button, but you need to remove the Button from there.
Also, If you need to manipulate the Visibility of a control from the ViewModel layer, you'd better use a Boolean property and the BooleanToVisibilityConverter in XAML:
ViewModel:
public bool IsControlVisible {get;set;} //Don't forget INotifyPropertyChanged!!
XAML:
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisConverter"/>
</Window.Resources>
<DockPanel Visibility="{Binding IsControlVisible, Converter={StaticResource BoolToVisConverter}}"/>
The problem is that you're binding your DockPanel to the boolean property of your view model, and then setting the Visiblity property of your UI element to the IsControlVisible property of the datacontext (which doesn't exist).
Change to:
private void SetDataContexts()
{
tvwDockPanel.DataContext = btnSelectWaferViewModel;
}
I have the following WPF Combobox:
<Window.Resources>
<CollectionViewSource x:Key="performanceItemsource" Source="{Binding Path=SelectedReport.Performances}" >
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Name"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
...
<ComboBox Name="cbxPlanPerf" Grid.ColumnSpan="2"
SelectedValuePath="MSDPortfolioID" DisplayMemberPath="Name"
SelectedValue="{Binding Path=PlanPerfID}"
ItemsSource="{Binding Source={StaticResource performanceItemsource}}"/>
The Source for the CollectionViewSource is:
public List<MSDExportProxy> Performances
{
get
{
if (Portfolio != null)
{
return (from a in Portfolio.Accounts where a.MSDPortfolioID != null select new MSDExportProxy(a))
.Concat<MSDExportProxy>(from g in Portfolio.Groups where g.MSDPortfolioID != null select new MSDExportProxy(g))
.Concat<MSDExportProxy>(from p in new[] { Portfolio } where p.MSDPortfolioID != null select new MSDExportProxy(p))
.ToList<MSDExportProxy>();
}
return new List<MSDExportProxy>();
}
}
The bound property PlanPerfID is a string.
I move between records using a ListBox control. The ComboBox works fine if the previous record had no items in its ComboBox.ItemsSource. If there were any items in the previous record's ComboBox.ItemsSource then the new record won't find its matching item in the ItemsSource collection. I've tried setting the ItemsSource in both XAML and the code-behind, but nothing changes this odd behavior. How can I get this darn thing to work?
Try using ICollectionViews in combination with IsSynchronizedWithCurrentItem property when handling lists / ObservableCollection in Xaml. The ICollectionView in the viewmodel can handle all the things needed, e.g. sorting, filtering, keeping track of selections and states.
Xaml:
<Window x:Class="ComboBoxBinding.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 />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0"
ItemsSource="{Binding Reports}"
DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True" />
<ComboBox Grid.Column="1"
ItemsSource="{Binding CurrentReport.Performances}"
DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True" />
</Grid>
</Window>
ViewModel:
public class ViewModel : INotifyPropertyChanged
{
private readonly IReportService _reportService;
private ObservableCollection<ReportViewModel> _reports = new ObservableCollection<ReportViewModel>();
private PerformanceViewModel _currentPerformance;
private ReportViewModel _currentReport;
public ObservableCollection<ReportViewModel> Reports
{
get { return _reports; }
set { _reports = value; OnPropertyChanged("Reports");}
}
public ReportViewModel CurrentReport
{
get { return _currentReport; }
set { _currentReport = value; OnPropertyChanged("CurrentReport");}
}
public PerformanceViewModel CurrentPerformance
{
get { return _currentPerformance; }
set { _currentPerformance = value; OnPropertyChanged("CurrentPerformance");}
}
public ICollectionView ReportsView { get; private set; }
public ICollectionView PerformancesView { get; private set; }
public ViewModel(IReportService reportService)
{
if (reportService == null) throw new ArgumentNullException("reportService");
_reportService = reportService;
var reports = _reportService.GetData();
Reports = new ObservableCollection<ReportViewModel>(reports);
ReportsView = CollectionViewSource.GetDefaultView(Reports);
ReportsView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
ReportsView.CurrentChanged += OnReportsChanged;
ReportsView.MoveCurrentToFirst();
}
private void OnReportsChanged(object sender, EventArgs e)
{
var selectedReport = ReportsView.CurrentItem as ReportViewModel;
if (selectedReport == null) return;
CurrentReport = selectedReport;
if(PerformancesView != null)
{
PerformancesView.CurrentChanged -= OnPerformancesChanged;
}
PerformancesView = CollectionViewSource.GetDefaultView(CurrentReport.Performances);
PerformancesView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
PerformancesView.CurrentChanged += OnPerformancesChanged;
PerformancesView.MoveCurrentToFirst();
}
private void OnPerformancesChanged(object sender, EventArgs e)
{
var selectedperformance = PerformancesView.CurrentItem as PerformanceViewModel;
if (selectedperformance == null) return;
CurrentPerformance = selectedperformance;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I found a quick and dirty solution to my problem. I just happen to have a public NotifyPropertyChanged() method on my Report entity and I discovered that if I called SelectedReport.NotifyPropertyChanged("PlanPerfID") in the Report ListBox's SelectionChanged event that it was enough of a jolt to get the ComboBox to re-evaluate and find its matching item in the ItemsSource. Yeah, it's KLUGE...
UPDATE: I also wound up needing to add SelectedReport.NotifyPropertyChanged("Performances") for some situations...
UPDATE 2: Okay, turns out the above wasn't bullet proof and I ran across a situation that broke it so I had to come up with a better workaround:
Altered the SelectedReport property in the Window's code-behind, adding a private flag (_settingCombos) to keep the Binding from screwing up the bound values until the dust has settled from changin the ItemSource:
private bool _settingCombos = false;
private Report _SelectedReport;
public Report SelectedReport
{
get { return _SelectedReport; }
set
{
_settingCombos = true;
_SelectedReport = value;
NotifyPropertyChanged("SelectedReport");
}
}
Created a proxy to bind to in the Window code-behind that will refuse to update the property's value if the _settingCombos flag is true:
public string PlanPerfID_Proxy
{
get { return SelectedReport.PlanPerfID; }
set
{
if (!_settingCombos)
{
SelectedReport.PlanPerfID = value;
NotifyPropertyChanged("PlanPerfID_Proxy");
}
}
}
Added an extra Notification in the Report ListBox's SelectionChanged event along with code to reset the _settingCombos flag back to false:
private void lbxReports_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//KLUGE: Couldn't get the ComboBoxes associated with these properties to work right
//this forces them to re-evaluate after the Report has loaded
if (SelectedReport != null)
{
NotifyPropertyChanged("PlanPerfID_Proxy");
_settingCombos = false;
}
}
Bound the ComboBox to the PlanPerfID_Proxy property (instead of directly to the SelectedReport.PlanPerfID property.
Wow, what a hassle! I think that this is simply a case of .NET's binding logic getting confused by the dynamic nature of the ComboBox.ItemSource, but this seems to have fixed it. Hope it helps someone else.