wpf toolkit data grid - wpf

hello i'm building a wpf app with data grids,
the pattern is model view view model.
all og my screens contains a contentcontrol, and i just assign him the view model, that have a suitable data template,
anyway, my problem is with combo box column, the data context is the presented entity, and i need it to be the view model.
whats the best solution?

I'm using another datagrid, but it might be similar. The way i did it was like that:
in the XAML, i defined an ObjectDataProvider in the resources:
<ObjectDataProvider x:Key="VM" ObjectInstance="{x:Null}" x:Name="vm"/>
then after assigning the DataContext (either the constructor or the DataContextChanged event), i did this:
(this.Resources["VM"] as ObjectDataProvider).ObjectInstance = this.DataContext;
In the Combobox xaml, i used that as binding source:
ItemsSource="{Binding Source={StaticResource VM}, Path=SomeItems, Mode=OneWay}"
Not sure if it works for the microsoft datagrid, but i guess it's worth a try.

this is how I used ViewModel with ComboBoxes, the DataContext is the ViewModel, not the underlying entity (List<Person>).
ViewModel (Person is a Simple class with Name and Age):
public class PeopleViewModel : INotifyPropertyChanged
{
private List<Person> _peopleList;
private Person _selectedPerson;
public PeopleViewModel()
{
// initialize with sample data
_peopleList = getPeopleList();
}
// gets sample data
private List<Person> getPeopleList()
{
var result = new List<Person>();
for (int i = 0; i < 10; i++)
{
result.Add(new Person("person " + i, i));
}
return result;
}
public List<Person> PeopleList
{
get { return _peopleList; }
}
public Person SelectedPerson
{
get { return _selectedPerson; }
set
{
if (_selectedPerson == value) return;
_selectedPerson = value;
// required so that View know about changes
OnPropertyChanged("SelectedPerson");
}
}
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
// WPF will listen on this event for changes
public event PropertyChangedEventHandler PropertyChanged;
}
XAML for ComboBox:
<ComboBox Name="cmbEnum" Width="150" ItemsSource="{Binding Path=PeopleList}" SelectedValue="{Binding Path=SelectedPerson}" SelectedValuePath="" DisplayMemberPath="Name" ></ComboBox>
And in code behind I can do:
public Window2()
{
InitializeComponent();
vm = new PeopleViewModel();
// we are listening on changes of ViewModel, not ComboBox
vm.PropertyChanged += new PropertyChangedEventHandler(vm_PropertyChanged);
this.DataContext = vm;
}
void vm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "SelectedPerson")
{
MessageBox.Show(vm.SelectedPerson.Age.ToString());
}
}
// button1_Click should be probably replaced by Command
private void button1_Click(object sender, RoutedEventArgs e)
{
// sample showing that GUI is updated when ViewModel changes
vm.SelectedPerson = vm.PeopleList[2];
}
Hope this helps, I'm quite new to WPF, I'd like to hear any feedback if this is the right way to use MVVM, I think it's quite elegant since you only deal with the ViewModel and Model in code, and the View can be replaced.

I Found that the best way of implementing this is define some external class for all lookups that i use in grid and embedd them in the template as a static resource

We ended up having classes with static properties for each of of our combo box lists:
(you can't make the class itself static otherwise XAML won't be able to open it, but you won't get compile errors)
For example:
public class ZoneList
{
private static readonly IList<Zone> _Items = new List<Zone>();
public static IList<Zone> Items
{
get { return _Items; }
}
}
and then in XAML:
<UserControl.Resources>
<ResourceDictionary>
<ObjectDataProvider x:Key="myZoneList" ObjectType="{x:Type StaticLists:ZoneList}"/>
</ResourceDictionary>
</UserControl.Resources>
<ComboBox ItemsSource="{Binding Path=Items, Source={StaticResource myZoneList}}"></ComboBox>

Related

Populate ListBox on Selecting TreeView node in WPF using MVVM

I am developing an application in WPF in which I want to populate a ListBox on selecting a node of a TreeView. I have populated the TreeView using MVVM pattern. It actually contains the the drives (C:\, D:...) and their corresponding sub-folders. The sub-folders are the nodes. On selecting these nodes the respective files should be shown in the ListBox. I know the C# code to get all the files in a folders, I have also implemented the same. However, I am not getting any clue to map them, so that on selecting the nodes the files in them should get reflected in the ListBox.
Can any please help me in this regard? The application is being developed in MVVM pattern and I need in the same pattern itself.
First add a Files collection class to your folder class (used in the tree view)
public class FolderItem
{
// other class code
private ObservableCollection<File> _Files = null;
public ObservableCollection<File> Files
{
get
{
if (_Files == null) _Files = GetFiles();
return _Files;
}
set
{
_Files = value;
}
}
}
Then bind the listbox to the selected treeview item.
<ListBox ItemsSource="{Binding ElementName=myTreeView, Path=SelectedItem.Files}"/>
You might have quite a number of files and folders so I think I would be inclined to lazy load as much as I could.
That means the viewmodel doesn't need to go iterate through the entire hard drive initially but you need some way of acting when selecteditem changes.
You can't bind selecteditem to a viewmodel because it's read only.
I would therefore use a behaviour like:
Data binding to SelectedItem in a WPF Treeview
Bind a SelectedFolder using that.
In the setter of SelectedFolder go get the list of folders and files for that folder and fill the two collections. One is the child collection of that selecteditem - for it's folders.
The other is an observableCollection for the files to see in the listbox.
Make that a propfull and implement inotifyproprtychanged so when I set it to a new collection it notifies the ui.
Bind that collection to the itemssource of the listbox.
Basicly The MVVM pattern uses three Layers :
The Model : Basicly it contains the Model classes and the business logic to get and manipulate Data information.
The ViewModel : It acts as an intermediate layer between the Model and the Views, it is attached to the different views.
The views : The différent views of the app.
Here an example how to fill a Window with list of Drives and Files.
Class BindableBaseViewModel
namespace TalkRepeater.ViewModel
{
public class BindableBaseViewModel : DependencyObject,INotifyPropertyChanged
{
protected virtual void SetProperty<T>(ref T member, T val,[CallerMemberName] string propertyName = null)
{
if (object.Equals(member, val)) return;
member = val;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
}
Class ViewModel
public class FoldersControlViewModel : BindableBaseViewModel
{
private ObservableCollection<Folders> _listFolders;
private ObservableCollection<Folders> _listFiles;
public FoldersControlViewModel()
{
FoldersBusinessObject vbo =new FoldersBusinessObject()
vbo.FillFolders();
ListFolders = FileBusinessObject.ListFolders;
}
public ObservableCollection<Folders> ListFolders
{
get
{
return _listFolders;
}
set
{
_listFolders = value;
OnPropertyChanged("ListFolders");
}
}
public ObservableCollection<Folders> ListFiles
{
get
{
return _listFiles;
}
set
{
_listFiles = value;
OnPropertyChanged("ListFiles");
}
}
Public void FillListFiles()
{
/*ListFiles= Cod to fill ListFiles*/
}
}
Class BusinessObject
public class FoldersBusinessObject
{
private ObservableCollection<Folders> _ListFolders;
public void FillFolders()
{
/* ListFolders= Code To fill the collection ListFolders */
}
public ObservableCollection<Folders> ListFolders
{
get
{
return _ListFolders;
}
set
{
_ListFolders = value;
}
}
}
Foldersview
<Window x:Class="Foldersview"
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"
d:DesignHeight = "300" Width="1007" Height="606">
<Grid Margin="10" >
<Canvas x:Name="canvasFolders" Margin="-10,0,912,10">
<TreeView x:Name="TreevFolders" ItemsSource="{Binding Path=ListFolders, Mode=TwoWay}" Canvas.Top="5" Canvas.Left="17" Width="142" Height="561"
SelectedItemChanged="TreevFolders_SelectedItemChanged" >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=ListFolders}">
<TextBlock Text="{Binding Path=FileName}">
</TextBlock>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Canvas>
<Canvas Margin="159,10,0,10">
<Listview x:Name="Listview1" ItemsSource="{Binding ListFiles, Mode=TwoWay}" >
</Listview>
</Canvas>
</Grid>
</Window>
Class Foldersview Code Behind
public partial class Foldersview : Window
{
private void TreevFolders_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
FoldersControlViewModel vmd = (FoldersControlViewModel)this.DataContext;
vmd.FillListFiles ();
}
}
Class Mainwindow
public class MainWindowViewModel : BindableBase
{
private FoldersControlViewModel FoldersviewModel;
public MainWindowViewModel()
{
FoldersviewModel = new FoldersControlViewModel();
Foldersview=new Foldersview();
Foldersview.Datacontext=FoldersviewModel;
}
}
Cordialy

How to Access SelectedItem in one Page on a Separate Page

I have two datagrids separately on two pages, say a parentgrid on a parent-page and a childgrid on a child-page. how to access the selecteditem of parent-page to the child-page ?
when the both the datagrids are placed on the same page, the selecteditem works. but when I place the grids separately on each page, it doesn't work.
XAML for the ParentPage
<Grid.Datacontext>
<local:MainViewModel/>
</Grid.Datacontext>
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Persons}" SelectedItem="{Binding SelectedHost, Mode=TwoWay}" SelectionChanged="DataGrid_SelectionChanged"/>
codebehind for the ParentPage
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ChildPage _page = new ChildPage();
this.NavigationService.Navigate(_page);
}
XAML for child page
<DataGrid x:Name="ChildDatagrid" Margin="12,104,81,266" ItemsSource="{Binding Details}"/>
MainViewModel
//Datacontext
public MainViewModel()
{
this.Persons = Person.GetPersons();
}
// for Person Datagrid
private ObservableCollection<Person> personValues;
public ObservableCollection<Person> Persons
{
get { return personValues; }
set { this.SetProperty<ObservableCollection<Person>>(ref this.personValues, value); }
}
//for the PersonDetails datagrid
public ObservableCollection<PersonDetails> Details
{
get
{
if (this.Selectedperson == null)
{
return null;
}
return this.LoadDetails(this.Selectedperson.PersonID);
}
}
// method to load the persondetails data
private ObservableCollection<PersonDetails> LoadDetails(int personID)
{
ObservableCollection<PersonDetails> details = new ObservableCollection<PersonDetails>();
foreach (PersonDetails detail in PersonDetails.GetDetails().Where(item => item.PersonID == personID))
{
details.Add(detail);
}
return details;
}
// SelectedPerson Property
private Person selectedPersonValue;
public Person Selectedperson
{
get { return selectedPersonValue; }
set
{
this.SetProperty<Person>(ref this.selectedPersonValue, value);
this.RaiseNotification("Details");
}
}
You should make a ViewModel or Object, pass them into both pages and bind your grids to it. This way they will stay in sync.
Alternate option is to use and EventAggregator to sent messages between your pages.
If you're using WPF you should take a look into Prism. A lot of build-in functionality exists.
EDIT: Post changed to reflect new information;
I've never used the NavigationService with WPF before, so I'm not 100% sure what is available to you, so apologies if I miss something.
However, if you move your Details Collection to your Child Form, and make it a standard property;
private ObservableCollection<PersonDetails> _Details;
public ObservableCollection<PersonDetails> Details {
get { return _Details; }
set {
if (value.Equals(_Details) == false)
{
_Details = value;
OnPropertyChanged("Details");
}
}
}
Assuming you have a reference to your MainViewModel, you can then navigate to your page using;
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ChildPage _page = new ChildPage();
_page.Details = MainViewModel.LoadDetails(PersonID);
this.NavigationService.Navigate(_page);
}
As a note, I don't like the idea of calling the Navigiate Service from the Code behind, as this makes the code messy and difficult to test.
Jesse Liberty has a nice post on using the MVVM Light Framework, and allowing the ViewModel to call the Navigate Service Directly;
http://jesseliberty.com/2012/01/17/calling-navigate-from-the-view-model/

ComboBox two way binding to selecteditem

I have spent considerable amount of time investigating this problem. Any help would be greatly appreciated.
I have a WPF ComboBox declared like this.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Click="Button_Click">Click Me</Button>
<ComboBox ItemsSource="{Binding ListOfValues}" SelectedItem="{Binding MySelectedItem}" Grid.Row="1">
</ComboBox>
<CheckBox IsChecked="{Binding IsValueChecked}" Grid.Row="2"></CheckBox>
</Grid>
In my code behind, i have these properties and i am implementing the INotifyPropertyChanged
public Window1()
{
InitializeComponent();
ListOfValues = new List<string>();
ListOfValues.Add("apple");
ListOfValues.Add("ball");
ListOfValues.Add("cat");
ListOfValues.Add("dog");
MySelectedItem = "cat";
IsValueChecked = true;
}
public List<string> ListOfValues
{
get
{
return _listOfValues;
}
set
{
_listOfValues = value;
OnPropertyChanged("ListOfValues");
}
}
public string MySelectedItem
{
get
{
return _selectedValueString;
}
set
{
_selectedValueString = value;
OnPropertyChanged("MySelectedItem");
}
}
public bool IsValueChecked
{
get
{
return _isVlaueChanged;
}
set
{
_isVlaueChanged = value;
OnPropertyChanged("IsValueChecked");
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MySelectedItem = "dog";
IsValueChecked = !IsValueChecked;
}
The button click event changes the MySelectedItem which is bound to the SelectedItem property of the combobox. But upon the button click nothing gets selected in the combobox. I dont understand why. This happens even if i set explicitly Mode=TwoWay. Please suggest. Note that my datacontext is set to self, so i have confirmed that data binding is happening properly by adding a checkbox
EDIT: Note that this happens in a sample WPF project. But my original project where i want this to work is a winforms app. I am using the elementhost to embed my wpf control. Is that making a difference?
The selected item needs to be set to an object in the list you have it bound to. settings it to a string with a matching value won't work. So try this:
foreach(string animal in ListOfValues)
{
if( animal == "dog")
this.MySelectedItem = animal;
}
I tried to reproduce your problem and I have some questions. Can you please show me your implementation of OnPropertyChanged? When I have a look at the MSDN (http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.onpropertychanged.aspx) this function requires a DependencyPropertyChangedEventArgs as the first parameter, not a string. And in addition, OnPropertyChanged is for notifying about changes in Dependency Properties, not for normal properties.
So I think you overloaded that method to support INotifyPropertyChanged, right?
I tried to implement a working example, this is the result:
public partial class TestWindow2 : Window, INotifyPropertyChanged
{
public TestWindow2()
{
InitializeComponent();
ListOfValues = new List<string> { "apple", "ball", "cat", "dog" };
MySelectedItem = "cat";
IsValueChecked = true;
this.DataContext = this;
}
...
public string MySelectedItem
{
get
{
return _selectedValueString;
}
set
{
_selectedValueString = value;
RaisePropertyChanged("MySelectedItem");
}
}
...
private void Button_Click(object sender, RoutedEventArgs e)
{
MySelectedItem = "dog";
IsValueChecked = !IsValueChecked;
}
private void RaisePropertyChanged(String name)
{
if( this.PropertyChanged != null ) this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Works perfectly for me. When I click the button, dog becoms the selected item in the combo box and the checkbox toggles its state.
If your items are a reference type (and you are just using string for an example), check that the Equals() method is returning what you expect. You might need to override the Equals method (eg this.ID ==other.ID or something like that) to get the correct behavior.

WPF MVVM TreeView SelectedItem

This cannot be this difficult. The TreeView in WPF doesn't allow you to set the SelectedItem, saying that the property is ReadOnly. I have the TreeView populating, even updating when it's databound collection changes.
I just need to know what item is selected. I am using MVVM, so there is no codebehind or variable to reference the treeview by. This is the only solution I have found, but it is an obvious hack, it creates another element in XAML that uses ElementName binding to set itself to the treeviews selected item, which you must then bind your Viewmodel too. Several other questions are asked about this, but no other working solutions are given.
I have seen this question, but using the answer given gives me compile errors, for some reason I cannot add a reference to the blend sdk System.Windows.Interactivity to my project. It says "unknown error system.windows has not been preloaded" and I haven't yet figured out how to get past that.
For Bonus Points: why the hell did Microsoft make this element's SelectedItem property ReadOnly?
You should not really need to deal with the SelectedItem property directly, bind IsSelected to a property on your viewmodel and keep track of the selected item there.
A sketch:
<TreeView ItemsSource="{Binding TreeData}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
public class TViewModel : INotifyPropertyChanged
{
private static object _selectedItem = null;
// This is public get-only here but you could implement a public setter which
// also selects the item.
// Also this should be moved to an instance property on a VM for the whole tree,
// otherwise there will be conflicts for more than one tree.
public static object SelectedItem
{
get { return _selectedItem; }
private set
{
if (_selectedItem != value)
{
_selectedItem = value;
OnSelectedItemChanged();
}
}
}
static virtual void OnSelectedItemChanged()
{
// Raise event / do other things
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
if (_isSelected)
{
SelectedItem = this;
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
A very unusual but quite effective way to solve this in a MVVM-acceptable way is the following:
Create a visibility-collapsed ContentControl on the same View the TreeView is. Name it appropriately, and bind its Content to some SelectedSomething property in viewmodel. This ContentControl will "hold" the selected object and handle it's binding, OneWayToSource;
Listen to the SelectedItemChanged in TreeView, and add a handler in code-behind to set your ContentControl.Content to the newly selected item.
XAML:
<ContentControl x:Name="SelectedItemHelper" Content="{Binding SelectedObject, Mode=OneWayToSource}" Visibility="Collapsed"/>
<TreeView ItemsSource="{Binding SomeCollection}"
SelectedItemChanged="TreeView_SelectedItemChanged">
Code Behind:
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
SelectedItemHelper.Content = e.NewValue;
}
ViewModel:
public object SelectedObject // Class is not actually "object"
{
get { return _selected_object; }
set
{
_selected_object = value;
RaisePropertyChanged(() => SelectedObject);
Console.WriteLine(SelectedObject);
}
}
object _selected_object;
You can create an attached property that is bindable and has a getter and setter:
public class TreeViewHelper
{
private static Dictionary<DependencyObject, TreeViewSelectedItemBehavior> behaviors = new Dictionary<DependencyObject, TreeViewSelectedItemBehavior>();
public static object GetSelectedItem(DependencyObject obj)
{
return (object)obj.GetValue(SelectedItemProperty);
}
public static void SetSelectedItem(DependencyObject obj, object value)
{
obj.SetValue(SelectedItemProperty, value);
}
// Using a DependencyProperty as the backing store for SelectedItem. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(TreeViewHelper), new UIPropertyMetadata(null, SelectedItemChanged));
private static void SelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is TreeView))
return;
if (!behaviors.ContainsKey(obj))
behaviors.Add(obj, new TreeViewSelectedItemBehavior(obj as TreeView));
TreeViewSelectedItemBehavior view = behaviors[obj];
view.ChangeSelectedItem(e.NewValue);
}
private class TreeViewSelectedItemBehavior
{
TreeView view;
public TreeViewSelectedItemBehavior(TreeView view)
{
this.view = view;
view.SelectedItemChanged += (sender, e) => SetSelectedItem(view, e.NewValue);
}
internal void ChangeSelectedItem(object p)
{
TreeViewItem item = (TreeViewItem)view.ItemContainerGenerator.ContainerFromItem(p);
item.IsSelected = true;
}
}
}
Add the namespace declaration containing that class to your XAML and bind as follows (local is how I named the namespace declaration):
<TreeView ItemsSource="{Binding Path=Root.Children}"
local:TreeViewHelper.SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}"/>
Now you can bind the selected item, and also set it in your view model to change it programmatically, should that requirement ever arise. This is, of course, assuming that you implement INotifyPropertyChanged on that particular property.
Use the OneWayToSource binding mode. This doesn't work. See edit.
Edit: Looks like this is a bug or "by design" behavior from Microsoft, according to this question; there are some workarounds posted, though. Do any of those work for your TreeView?
The Microsoft Connect issue: https://connect.microsoft.com/WPF/feedback/details/523865/read-only-dependency-properties-does-not-support-onewaytosource-bindings
Posted by Microsoft on 1/10/2010 at 2:46 PM
We cannot do this in WPF today, for the same reason we cannot support
bindings on properties that are not DependencyProperties. The runtime
per-instance state of a binding is held in a BindingExpression, which
we store in the EffectiveValueTable for the target DependencyObject.
When the target property is not a DP or the DP is read-only, there's
no place to store the BindingExpression.
It's possible we may some day choose to extend binding functionality
to these two scenarios. We get asked about them pretty frequently. In
other words, your request is already on our list of features to
consider in future releases.
Thanks for your feedback.
I decided to use a combination of code behind and viewmodel code. the xaml is like this:
<TreeView
Name="tvCountries"
ItemsSource="{Binding Path=Countries}"
ItemTemplate="{StaticResource ResourceKey=countryTemplate}"
SelectedValuePath="Name"
SelectedItemChanged="tvCountries_SelectedItemChanged">
Code behind
private void tvCountries_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var vm = this.FindResource("vm") as ViewModels.CoiEditorViewModel;
if (vm != null)
{
var treeItem = sender as TreeView;
vm.TreeItemSelected = treeItem.SelectedItem;
}
}
And in the viewmodel there is a TreeItemSelected object which you can then access in the viewmodel.
You can always create a DependencyProperty that uses ICommand and listen to the SelectedItemChanged event on the TreeView. This can be a bit easier than binding IsSelected, but I imagine you will wind up binding IsSelected anyway for other reasons. If you just want to bind on IsSelected you can always have your item send a message whenever IsSelected changes. Then you can listen to those messages anyplace in your program.

Silverlight MVVM binding updates fire in undesired order

Scenario: In a Silverlight 4 MVVM project, we have a ListBox control containing items, the selected item is two-way-bound to the appropriate property in the ViewModel. Another control (for example reasons, I've stripped it down to a single TextBox) is data bound to the selected item's content. The value should update on leave/focus lost.
Problem: When the value in the TextBox is changed and we leave that TextBox by pressing the Tab key, everything works as desired - the value is updated. However, if the user clicks on a different item in the ListBox, then the SelectedItem setter is fired before the content of TextBox setter is fired, leaving no chance to handle the user input.
You can see in debugger, when adding breakpoints to the property setters, that the new ListView selection is applied first, before the TextBox update is processed.
Desired behavior: We need to know that the currently selected item was modified before the user has selected another item. It's not desired to have a custom update trigger which would notify on each key press (we know that's possible).
Can you help?
Code (a very simple example):
ViewModel
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ItemViewModel : ViewModelBase
{
private string _content;
public ItemViewModel(string initContent)
{
_content = initContent;
}
public string Content
{
get
{
return _content;
}
set
{
if (_content != value)
{
_content = value;
OnPropertyChanged("Content");
}
}
}
}
public class MainViewModel : ViewModelBase
{
private ObservableCollection<ItemViewModel> _items =
new ObservableCollection<ItemViewModel>();
private ItemViewModel _selectedViewModel;
public ObservableCollection<ItemViewModel> Items
{
get
{
return _items;
}
}
public ItemViewModel SelectedItem
{
get
{
return _selectedViewModel;
}
set
{
if (_selectedViewModel != value)
{
_selectedViewModel = value;
OnPropertyChanged("SelectedItem");
}
}
}
}
XAML
<Grid x:Name="LayoutRoot" Background="White">
<ListBox Height="100"
HorizontalAlignment="Left"
Margin="12,12,0,0"
VerticalAlignment="Top"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
DisplayMemberPath="Content"
Width="220" />
<TextBox Height="23"
HorizontalAlignment="Left"
Margin="12,118,0,0"
Text="{Binding SelectedItem.Content, Mode=TwoWay}"
VerticalAlignment="Top"
Width="220" />
</Grid>
XAML Code Behind
public MvvmTestView()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MvvmTestView_Loaded);
}
void MvvmTestView_Loaded(object sender, RoutedEventArgs e)
{
MainViewModel viewModel = new MainViewModel();
viewModel.Items.Add(new ItemViewModel("Hello StackOverflow"));
viewModel.Items.Add(new ItemViewModel("Thanks to Community"));
DataContext = viewModel;
}
UPDATE 1
I present a self designed solution for you to check out, which will be probably be the accepted one, I still want to encourage you to make comments and give your hints. Thanks.
You could add a behavior to your textbox to updated the binding every time the text is changed in the textbox. Maybe this solved your problems.
Here´s the code for the Behavior class:
public class UpdateTextBindingOnPropertyChanged : Behavior<TextBox> {
// Fields
private BindingExpression expression;
// Methods
protected override void OnAttached() {
base.OnAttached();
this.expression = base.AssociatedObject.GetBindingExpression(TextBox.TextProperty);
base.AssociatedObject.TextChanged+= OnTextChanged;
}
protected override void OnDetaching() {
base.OnDetaching();
base.AssociatedObject.TextChanged-= OnTextChanged;
this.expression = null;
}
private void OnTextChanged(object sender, EventArgs args) {
this.expression.UpdateSource();
}
}
Heres the XAML:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="Namespace of the class where UpdateTextBindingOnPropertyChanged is defined"
<TextBox Text="{Binding SelectedItem.Content, Mode=TwoWay}">
<i:Interaction.Behaviors>
<local:UpdateTextBindingOnPropertyChanged />
</i:Interaction.Behaviors>
</TextBox >
This is one solution we currently came up with. It has the advantage that it separates different tasks to the appropriate layer. For example, the View enforces an update of the binding, while the ViewModel tells the View to do so. Another advantage is that its handled synchronously, which would for example allow to check the content right before switching away, and the call-stack remains unchanged without raising "External Code" (Going over Dispatcher or even DispatcherTimer would do so) which is better for maintenance and flow control. A disadvantage is the new Event which has to be bound and handled (and finally unbound. I present an anonymous handler only for example reasons).
How to get there?
In ViewModelBase, implement a new ForceBindingUpdate event:
public abstract class ViewModelBase : INotifyPropertyChanged
{
// ----- leave everything from original code ------
public event EventHandler ForceBindingUpdate;
protected void OnForceBindingUpdate()
{
var handler = ForceBindingUpdate;
if (handler != null)
handler(this, EventArgs.Empty);
}
}
In MainViewModel, update the setter of the SelectedItem property:
set // of SelectedItem Property
{
if (_selectedViewModel != value)
{
// Ensure Data Update - the new part
OnForceBindingUpdate();
// Old stuff
_selectedViewModel = value;
OnPropertyChanged("SelectedItem");
}
}
Update the MvvmTestView Code Behind to implement the new event:
void MvvmTestView_Loaded(object sender, RoutedEventArgs e)
{
// remains unchanged
Mvvm.MainViewModel viewModel = new Mvvm.MainViewModel();
viewModel.Items.Add(new Mvvm.ItemViewModel("Hello StackOverflow"));
viewModel.Items.Add(new Mvvm.ItemViewModel("Thanks to Community"));
// Ensure Data Update by rebinding the content property - the new part
viewModel.ForceBindingUpdate += (s, a) =>
{
var expr = ContentTextBox.GetBindingExpression(TextBox.TextProperty);
expr.UpdateSource();
};
// remains unchanged
DataContext = viewModel;
}
Last but not least, the minimal XAML Update: Give the TextBox a name by adding x:Name="ContentTextBox" Attribute to the TextBoxs XAML.
Done.
Actually, I don't know if this is the cleanest solution, but it gets close to what we had in mind.
Maybe you could handle TextBox LostFocus then (instead of listening to every key press)?
Other idea would be to keep a proxy property on the ViewModel instead of directly binding to SelectedItem.Content and writing some code to make sure the item is updated.
Solution №1
public class LazyTextBox: TextBox
{
//bind to that property instead..
public string LazyText
{
get { return (string)GetValue(LazyTextProperty); }
set { SetValue(LazyTextProperty, value); }
}
public static readonly DependencyProperty LazyTextProperty =
DependencyProperty.Register("LazyText", typeof(string), typeof(LazyTextBox),
new PropertyMetadata(null));
//call this method when it's really nessasary...
public void EnsureThatLazyTextEqualText()
{
if (this.Text != this.LazyText)
{
this.LazyText = this.Text;
}
}
}
Solution №2 (works as magic :) )
public class MainViewModel : ViewModelBase
{
private ObservableCollection<ItemViewModel> _items =
new ObservableCollection<ItemViewModel>();
private ItemViewModel _selectedViewModel;
public ObservableCollection<ItemViewModel> Items { get { return _items; } }
public ItemViewModel SelectedItem
{
get { return _selectedViewModel; }
set
{
if (_selectedViewModel != value)
{
if (SelectedItem != null)
{
SelectedItem.Content = SelectedItem.Content;
}
_selectedViewModel = value;
// A little delay make no harm :)
var t = new DispatcherTimer();
t.Interval = TimeSpan.FromSeconds(0.1);
t.Tick += new EventHandler(t_Tick);
t.Start();
}
}
}
void t_Tick(object sender, EventArgs e)
{
OnPropertyChanged("SelectedItem");
(sender as DispatcherTimer).Stop();
}
}
I know that in MVVM we do not want to put code in code behind. But in this instance it hurts nothing as it is entirely maintained in the UI and SOP is maintained.
By putting a ghost element to take focus we can swap the focus back in forth forcing
the text box to commit its contents. So in the code behind we take care of the focus wiggle.
But yet we still are using a relay command Update Command to execute the save. So the order is good as the Click event fires wiggling the view. And then the relay command UpdateCommand will fire and the textbox is committed and ready for update.
<MenuItem Header="_Save"
Command="{Binding UpdateCommand}" Click="MenuItem_Click">
</MenuItem>
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
UIElement elem = Keyboard.FocusedElement as UIElement;
Keyboard.Focus(ghost);
Keyboard.Focus(elem);
}
Solution #3
public abstract class ViewModelBase : INotifyPropertyChanged
{
private List<string> _propNameList = new List<string>();
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
_propNameList.Add(propertyName);
var t = new DispatcherTimer();
t.Interval = TimeSpan.FromSeconds(0);
t.Tick += new EventHandler(t_Tick);
t.Start();
}
void t_Tick(object sender, EventArgs e)
{
if (_propNameList.Count > 0)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(_propNameList[0]));
_propNameList.Remove(_propNameList[0]);
}
}
}
PS: it's the same timer.. but this solution is more generic..

Resources