A newbie question, but I can't get it to work. In trying to use the answer to SO "How Should the View Model Close the form"
, I have a UserControl defined with:
<UserControl
.....
h:DialogCloser.DialogResult="{Binding DialogResult}"
>
which while designing shows:
Property 'DialogResult' is not attachable to elements of type 'UserControl'.
The DialogCloser is defined as:
public static class DialogCloser
{
public static readonly DependencyProperty DialogResultProperty =
DependencyProperty.RegisterAttached(
"DialogResult",
typeof(bool?),
typeof(DialogCloser),
new PropertyMetadata(DialogResultChanged));
private static void DialogResultChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var window = d as Window;
if (window != null)
window.DialogResult = e.NewValue as bool?;
}
public static void SetDialogResult(Window target, bool? value)
{
target.SetValue(DialogResultProperty, value);
}
}
The UserControl is opened by:
var win = new WindowDialog();
win.Title = title;
win.DataContext = datacontext;
return win.ShowDialog();
In the View Model for the UserControl, I have:
public new void DoExit()
{
DialogResult = true;
}
private bool? dialogresult;
public bool? DialogResult
{
get
{
return dialogresult;
}
set
{
if (dialogresult != value)
{
dialogresult = value;
OnPropertyChanged("DialogResult");
}
}
}
When using DoExit(), the modal dialog, my UserControl, does not close. What's wrong? And how do I get the designer (VS 2010) to not throw error?
Thanks is advance for any help.
Addendum:
public class ModalDialogService : IModalDialogService
{
public bool? ShowDialog(string title, object datacontext)
{
var win = new WindowDialog();
win.Title = title;
win.DataContext = datacontext;
return win.ShowDialog();
}
}
Note: If the "UserControl" is rather made as a "Window", the designer is happy, but then the error is:
"Window must be the root of the tree. Cannot add Window as a child of Visual."
Help somebody?
I am new to this, but putting everything together, I would suggest this as a possible answer for newbies like myself.
Does this comply with best MVVM practices?
First, as the designer stated "Property 'DialogResult' is not attachable to elements of type 'UserControl'.
My translation, a "control" is not a window--so it can't be closed. Therefore,
h:DialogCloser.DialogResult="{Binding DialogResult}"
is wrong. Remove it.
Second, my usercontrol is being displayed as the sole element of an otherwise plain window as
<Window x:Class="Nova5.UI.Views.Ink.WindowDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WindowDialog"
WindowStyle="SingleBorderWindow"
WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight">
<ContentPresenter x:Name="DialogPresenter" Content="{Binding .}"/>
</Window>
Now, the whole thing is initiated when the top most view model does
var dialog = new WindowDialog
{
Title = "It's me Margaret",
ShowInTaskbar = false,
Topmost = true,
ResizeMode = ResizeMode.NoResize
};
dialog.Owner = Application.Current.Windows.OfType<Window>().SingleOrDefault(x => x.IsActive);
ModalDialogService dialogService = new ModalDialogService();
dialogService.ShowDialog<PrescriptionWriterViewModel>(dialog,
new PrescriptionWriterViewModel(),
returnedViewModelInstance =>
{
if (dialog.DialogResult.HasValue && dialog.DialogResult.Value)
{
}
}
);
};
When Exit is clicked on the usercontrol, it performs by way of a DelegateCommand in its view model
public new void DoExit()
{
OnRequestClose();
}
public event EventHandler RequestClose;
void OnRequestClose()
{
EventHandler handler = this.RequestClose;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
This request to close is then handled in the initiating dialog service,
public void ShowDialog<TDialogViewModel>(IModalWindow view, TDialogViewModel viewModel, Action<TDialogViewModel> onDialogClose)
{
view.DataContext = viewModel;
IDialogViewModel vm = viewModel as IDialogViewModel;
if (vm !=null)
vm.RequestClose += (s,e)=> view.Close(); //RequestClose is the event in the ViewModel that the view will listen for.
if (onDialogClose != null)
{
view.Closed += (sender, e) => onDialogClose(viewModel);
}
view.Show();
}
When the view is closed, the onDialogClose(viewModel), then calls back to the topmost view model and completes the action specified with
returnedViewModelInstance =>
{
if (dialog.DialogResult.HasValue && dialog.DialogResult.Value)
{
}
}
So completes several intense hours of learning. I hope it is of use to somebody.
Related
I have a dialog that use, that i simple can't understand why it will not bind.
I use Joe White's DialogCloser, from this answer
public static class DialogCloser
{
public static readonly DependencyProperty DialogResultProperty =
DependencyProperty.RegisterAttached(
"DialogResult",
typeof(bool?),
typeof(DialogCloser),
new PropertyMetadata(DialogResultChanged));
private static void DialogResultChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var window = d as Window;
if (window != null)
window.DialogResult = e.NewValue as bool?;
}
public static void SetDialogResult(Window target, bool? value)
{
target.SetValue(DialogResultProperty, value);
}
}
I use it only in DialogWindow.xml
<Window x:Class="ATS.Views.Common.DialogWindow"
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"
Title="WindowDialog"
WindowStyle="SingleBorderWindow"
WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight"
xmlns:xc="clr-namespace:ATS.Views.Common"
xc:DialogCloser.DialogResult="{Binding DialogResult}"
ResizeMode="NoResize">
<ContentPresenter x:Name="DialogPresenter" Content="{Binding .}"></ContentPresenter>
The Binding it self is on a abstract class that I want to use for dialog windows.
public class DialogViewModel : BaseViewModel
{
private bool? dialogResult;
public bool? DialogResult
{
get
{
return dialogResult;
}
set
{
dialogResult = value;
NotifyPropertyChange("DialogResult");
}
}
}
I open dialogs from ViewModels with this:
public bool? ShowDialog(string title, DialogViewModel dataContext)
{
var win = new DialogWindow();
win.Title = title;
win.DataContext = dataContext;
return win.ShowDialog();
}
Now what seems to happen, even tho i create new dialog windows, is that the binding it self only get added once, and therefor it will only work on the first dialog that is created.
Is it something to do with binding to an abstact class that makes the bindings not work, due to same namespace and names?
Edit:
Ended up using Mark's way of handling dialog boxes, which can be found here
What's the right approach to open a child window (for example, to modify a selected item on the main window) keeping MVVM in mind?
Here's what I have: MainWindow.xaml (and in MainWindow.xaml.cs it assigns MainVM as its own DataContext)
I would also like to have: ChildWindow.xaml and barebones ChildWindow.xaml.cs with ChildVM behind controls.
So, now:
How can I popup ChildWindow and pass some object Data to its
ChildVM?
Get the result (true/false) and result data (some complex
object) back to MainVM?
As a bonus, can changes in Data be observed
by MainVM while they are being worked on by ChildVM?
Here's what I tried - it doesn't solve everything, but is this even the right direction?
For (2), I created a subclass of Window, called DialogWindow, which has 3 DependencyProperties: Data (for input data), ResultData (for output data) and ResultValue (for a bool result).
ResultData and ResultValue are both set by the ChildVM of DialogWindow using Binding, and when ResultValue is set, the DialogWindow closes.
At the moment, the ChildWindow is launched (for all intents and purposes) from MainWindow.xaml.cs - kinda bad. I can then pass some input data, like so:
ChildDialogWindow w = new ChildDialogWindow();
w.Data = myDataObj;
So, now I need to have a property Data on ChildVM, and set in ChildDialogWindow.xaml.cs. Again, making .xaml.cs thicker.
I thought that maybe a better approach that avoids MainWindow.xaml.cs would be some kind of DialogService which is passed to MainVM as a dependency. But then, how can I pass values to the ChildVM?
Try this.
Make a DialogService.cs
public class DialogService
{
public void Show(FrameworkElement view, ChildViewModel ChildVM)
{
Window window = new Window();
window.Content = view;
window.DataContext = ChildVM;
// For closing this dialog using MVVM
ChildVM.RequestClose += delegate
{
window.Close();
};
window.Show();
}
}
Now in ChildVm class, add this
public ICommand CloseCommand
{
get
{
if (_closeCommand == null)
_closeCommand = new RelayCommand(param => this.OnRequestClose());
return _closeCommand;
}
}
public event EventHandler RequestClose;
void OnRequestClose()
{
EventHandler handler = this.RequestClose;
if (handler != null)
handler(this, EventArgs.Empty);
}
Now, this the way to launch this
public void OpenChildDailog()
{
DialogService service = new DialogService();
ChildViewModel childVM = new ChildViewModel();
childVM.Data = ; // Assign whatever you want
childVM.ResultData = ;
service.Show(new ChildView(), childVM);
// Now get the values when the child dailog get closed
var retVal = childVM.ResultValue;
}
I'm using the ICommand helper "RelayCommand," and pushing an IntPtr datatype to the new ViewModel (or use any other object.) Lots of cookie cutter stuff.
Main View:
<Button Command="{Binding DataContext.ShowObjectInfoCommand}" CommandParameter="{Binding ObjectOffset}" Content="{Binding Name}"/>
MainViewModel:
private RelayCommand _showObjectInfoCommand;
public RelayCommand ShowObjectInfoCommand { get { return _showObjectInfoCommand ?? (_showObjectInfoCommand = new RelayCommand(ExeShowObjectInfoCommand)); } set { } } //Draw Specific Item Table
void ExeShowObjectInfoCommand(object parameter)
{
ViewObjectInfo objInfo = new ViewObjectInfo();
IObjectOffsetParameter viewModel = objInfo.DataContext as IObjectOffsetParameter;
viewModel.ObjectOffset = (IntPtr)parameter;
objInfo.Show();
}
New ViewModel + interface:
interface IObjectOffsetParameter
{
IntPtr ObjectOffset { get; set; }
}
class ViewModelObjectInfo : ViewModelBase, IObjectOffsetParameter
{
public ViewModelObjectInfo()
{
}
private IntPtr _objectOffset; //Entity Offset
public IntPtr ObjectOffset
{
get { return _objectOffset; }
set { if (_objectOffset != value) { _objectOffset = value; RaisePropertyChanged("Offset"); } }
}
}
New View code-behind:
InitializeComponent();
ViewModelObjectInfo viewModel = new ViewModelObjectInfo();
this.DataContext = viewModel;
New View xaml:
<TextBlock Text="{Binding ObjectOffset}"/>
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..
I'm using the WPF datagrid, and have SelectionUnit="Cell" and SelectionMode="Extended". I'm also trying to adhere to the MVVM principals as much as I can.
I need my ViewModel to keep track of the current SelectedCells.
Life would be easy if I could just Bind its SelectedCells property to my ViewModel. Oddly enough, SelectedCells is only raised once - when we first select any cell in the grid.
MS explains it here: http://social.msdn.microsoft.com/Forums/en/wpf/thread/737117f4-6d20-4232-88cf-e52cc44d4431
Can anyone think of an MVVM-friendly approach to get around it?
Thanks!
I realized my last answer was for SelectedItems instead of SelectedCells, so I wrote a complete attached property class to do data binding for multiple SelectedCells which works as follows:
<controls:DataGrid ItemsSource="{StaticResource list}"
SelectionMode="Extended"
behaviors:DataGridSelectedCellsBehavior.SelectedCells="{Binding Path=SelectedGridCellCollection, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
I have a working source code and a demo project of it here.
Attached property behavior Code :
public class DataGridSelectedCellsBehavior
{
// Source : https://archive.codeplex.com/?p=datagridthemesfromsl
// Credit to : T. Webster, https://stackoverflow.com/users/266457/t-webster
public static IList<DataGridCellInfo> GetSelectedCells(DependencyObject obj)
{
return (IList<DataGridCellInfo>)obj.GetValue(SelectedCellsProperty);
}
public static void SetSelectedCells(DependencyObject obj, IList<DataGridCellInfo> value)
{
obj.SetValue(SelectedCellsProperty, value);
}
public static readonly DependencyProperty SelectedCellsProperty = DependencyProperty.RegisterAttached("SelectedCells", typeof(IList<DataGridCellInfo>), typeof(DataGridSelectedCellsBehavior), new UIPropertyMetadata(null, OnSelectedCellsChanged));
static SelectedCellsChangedEventHandler GetSelectionChangedHandler(DependencyObject obj)
{
return (SelectedCellsChangedEventHandler)obj.GetValue(SelectionChangedHandlerProperty);
}
static void SetSelectionChangedHandler(DependencyObject obj, SelectedCellsChangedEventHandler value)
{
obj.SetValue(SelectionChangedHandlerProperty, value);
}
static readonly DependencyProperty SelectionChangedHandlerProperty = DependencyProperty.RegisterAttached("SelectedCellsChangedEventHandler", typeof(SelectedCellsChangedEventHandler), typeof(DataGridSelectedCellsBehavior), new UIPropertyMetadata(null));
//d is MultiSelector (d as ListBox not supported)
static void OnSelectedCellsChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
if (GetSelectionChangedHandler(d) != null)
return;
if (d is DataGrid)//DataGrid
{
DataGrid datagrid = d as DataGrid;
SelectedCellsChangedEventHandler selectionchanged = null;
foreach (var selected in GetSelectedCells(d) as IList<DataGridCellInfo>)
datagrid.SelectedCells.Add(selected);
selectionchanged = (sender, e) =>
{
SetSelectedCells(d, datagrid.SelectedCells);
};
SetSelectionChangedHandler(d, selectionchanged);
datagrid.SelectedCellsChanged += GetSelectionChangedHandler(d);
}
//else if (d is ListBox)
//{
// ListBox listbox = d as ListBox;
// SelectionChangedEventHandler selectionchanged = null;
// selectionchanged = (sender, e) =>
// {
// SetSelectedCells(d, listbox.SelectedCells);
// };
// SetSelectionChangedHandler(d, selectionchanged);
// listbox.SelectionChanged += GetSelectionChangedHandler(d);
//}
}
}
View Model Code :
class DemoViewModel : INotifyPropertyChanged
{
private IList<DataGridCellInfo> selectedGridCellCollection = new List<DataGridCellInfo>();
public IList<DataGridCellInfo> SelectedGridCellCollection
{
get { return selectedGridCellCollection; }
set
{
selectedGridCellCollection = value;
NotifyPropertyChanged();
}
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Do you need the SelectedCells constantly data-binded, or just when the user hits the OK/Accept button? If you only need it at the end of whatever process the user is in you can bind the SelectedCells to the CommandParameter property of a Button, for example. The SelectedCells is an IList, and you know enough to just do a cast to whatever object type the selection actually is. The other option is messier, you can use an attached property, keeping the event-handling out of your Views. This attached property would handle either a ListBox or in your case a DataGrid (MultiSelector).
public class Attach
{
public static IList GetSelectedItems(DependencyObject obj)
{
return (IList)obj.GetValue(SelectedItemsProperty);
}
public static void SetSelectedItems(DependencyObject obj, IList value)
{
obj.SetValue(SelectedItemsProperty, value);
}
/// <summary>
/// Attach this property to expose the read-only SelectedItems property of a MultiSelector for data binding.
/// </summary>
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.RegisterAttached("SelectedItems", typeof(IList), typeof(Attach), new UIPropertyMetadata(new List<object>() as IList, OnSelectedItemsChanged));
static SelectionChangedEventHandler GetSelectionChangedHandler(DependencyObject obj)
{
return (SelectionChangedEventHandler)obj.GetValue(SelectionChangedHandlerProperty);
}
static void SetSelectionChangedHandler(DependencyObject obj, SelectionChangedEventHandler value)
{
obj.SetValue(SelectionChangedHandlerProperty, value);
}
static readonly DependencyProperty SelectionChangedHandlerProperty =
DependencyProperty.RegisterAttached("SelectionChangedHandler", typeof(SelectionChangedEventHandler), typeof(Attach), new UIPropertyMetadata(null));
//d is MultiSelector (d as ListBox not supported)
static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
if (GetSelectionChangedHandler(d) != null)
return;
if (d is MultiSelector)//DataGrid
{
MultiSelector multiselector = d as MultiSelector;
SelectionChangedEventHandler selectionchanged = null;
foreach (var selected in GetSelectedItems(d) as IList)
multiselector.SelectedItems.Add(selected);
selectionchanged = (sender, e) =>
{
SetSelectedItems(d, multiselector.SelectedItems);
};
SetSelectionChangedHandler(d, selectionchanged);
multiselector.SelectionChanged += GetSelectionChangedHandler(d);
}
else if (d is ListBox)
{
ListBox listbox = d as ListBox;
SelectionChangedEventHandler selectionchanged = null;
selectionchanged = (sender, e) =>
{
SetSelectedItems(d, listbox.SelectedItems);
};
SetSelectionChangedHandler(d, selectionchanged);
listbox.SelectionChanged += GetSelectionChangedHandler(d);
}}}
Usage in XAML:
<DataGrid ItemsSource="{Binding Path=SourceList}"
myControls:Attach.SelectedItems="{Binding Path=myMvvmSelectedItems, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectionMode="Extended" />
You might be interested in the BookLibrary sample application of the WPF Application Framework (WAF). It shows how to synchronize the DataGrid.SelectedItems with the ViewModel. This might be very similar to SelectedCells.
Somewhere between perfect MVVM bindings and full event handler codebehinds there is the gray area of interactivity EventTriggers (see Blend SDK) :)
If you put an eventtrigger to the datagrid, and set to "SelectionChanged" and pass the eventargs to a command (use an EventToCommand actiontrigger) you could get the selected items from the eventargs hopefully...
Or use the multibinding as said in the MS thread :)
I'm having some trouble understanding how command parameter binding works.
When I create an instance of the widget class before the call to InitializeComponent it seems to work fine. Modifications to the parameter(Widget) in the ExecuteCommand function will be "applied" to _widget. This is the behavior I expected.
If the instance of _widget is created after InitializeComponent, I get null reference exceptions for e.Parameter in the ExecuteCommand function.
Why is this? How do I make this work with MVP pattern, where the bound object may get created after the view is created?
public partial class WidgetView : Window
{
RoutedCommand _doSomethingCommand = new RoutedCommand();
Widget _widget;
public WidgetView()
{
_widget = new Widget();
InitializeComponent();
this.CommandBindings.Add(new CommandBinding(DoSomethingCommand, ExecuteCommand, CanExecuteCommand));
}
public Widget TestWidget
{
get { return _widget; }
set { _widget = value; }
}
public RoutedCommand DoSomethingCommand
{
get { return _doSomethingCommand; }
}
private static void CanExecuteCommand(object sender, CanExecuteRoutedEventArgs e)
{
if (e.Parameter == null)
e.CanExecute = true;
else
{
e.CanExecute = ((Widget)e.Parameter).Count < 2;
}
}
private static void ExecuteCommand(object sender, ExecutedRoutedEventArgs e)
{
((Widget)e.Parameter).DoSomething();
}
}
<Window x:Class="CommandParameterTest.WidgetView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WidgetView" Height="300" Width="300"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<StackPanel>
<Button Name="_Button" Command="{Binding DoSomethingCommand}"
CommandParameter="{Binding TestWidget}">Do Something</Button>
</StackPanel>
</Window>
public class Widget
{
public int Count = 0;
public void DoSomething()
{
Count++;
}
}
InitializeCompenent processes the xaml associated with the file. It is at this point in time that the CommandParameter binding is first processed. If you initialize your field before InitializeCompenent then your property will not be null. If you create it after then it is null.
If you want to create the widget after InitializeCompenent then you will need to use a dependency property. The dependency proeprty will raise a notification that will cause the CommandParameter to be updated and thus it will not be null.
Here is a sample of how to make TestWidget a dependency property.
public static readonly DependencyProperty TestWidgetProperty =
DependencyProperty.Register("TestWidget", typeof(Widget), typeof(Window1), new UIPropertyMetadata(null));
public Widget TestWidget
{
get { return (Widget) GetValue(TestWidgetProperty); }
set { SetValue(TestWidgetProperty, value); }
}
Even with the dependency property, you still need to call CommandManager.InvalidateRequerySuggested to force the CanExecute of the Command being evaluated.