Raising a user-defined event from a WPF control - wpf

I have a ListView control with support for incremental loading of large item collections. The loading of additional items is triggered when the user scrolls down to the bottom ListView's vertical scrollbar. I strive to use MVVM pattern in the implementation (no code-behind), but IMHO in this particular case it's not essential.
I am tinking about introducing a new event (e.g. NeedsMoreItems) and perhaps subclass ListView (ListViewWithIncrementalLoading) so the application won't need to do ScrollChanged events mapping. I haven't implemented custom events for XAML controls before, so I am not quite sure what's the easiest way to achive this. Do I need to create custom control? User control? Use attached events?
If you have code snippets from similar tasks, I will really appreciate it.

This is not the complete code because this was all very hard to develop and it's very much code (used in the software CCFinder), also it's not perfect, but it does it's job.
<CCFinder:AnimatedScrollViewer VerticalScrollBarVisibility="{Binding IsItemsFound, Converter={StaticResource __boolToVisibilityConverter}}" Margin="36,211,38,72"
ScrollChanged="ScrollViewer_ScrollChanged" HorizontalContentAlignment="Center" Focusable="False" x:Name="ScrolView1">
<ItemsControl Name="_itemsControl" ItemsSource="{Binding CurrentImages}"
...
In the codebehind:
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (e.VerticalOffset + e.ViewportHeight == e.ExtentHeight)
{
var scroller = ((ScrollViewer)sender);
scroller.ReleaseMouseCapture();
scroller.InvalidateScrollInfo();
((OverviewViewModel)this.DataContext).ShowMoreTriggered();
}
}
So here comes the important part: in the ViewModel class I set the MaximumImages property to a higher value... it begins with about 50 and is then raised to 251 and then to 500 when this is triggered again. In the setter of the MaximumImages, the WPF framework is notified that CurrentImages has changed, and in the CurrentImages getter I have a lot of clunky code that realizes that the MaximumImages number is now higher than before, and adds new items to CurrentImages (all in the getter). Not too nice, but it works and is mostly in the ViewModel class:
public void ShowMoreTriggered()
{
if (Photos != null && !ShowMoreTriggeredActive && MaximumImages < Photos.Count)
{
ShowMoreTriggeredActive = true;
ThreadPool.QueueUserWorkItem(delegate
{
Thread.Sleep(1000);
MaximumImages = MaximumImages < 251 ? 251 : 500;
Thread.Sleep(1500);
ShowMoreTriggeredActive = false;
});
}
}
private int _maximumImages;
public int MaximumImages
{
get
{
return _maximumImages;
}
set
{
_maximumImages = value;
InvokePropertyChanged("MaximumImages");
InvokePropertyChanged("CurrentImages");
}
}
Of course it would be more elegant to raise a user-defined event in case the scroll event reaches the lowest point, but I guess it would only produce more code, and not less, because it would certainly would be propagated from the ScrollChanged event anyways.

Related

Deferred loading of XAML

A project I'm working on has some rather complex XAML that is noticeably affecting visual performance. Quite a few controls are collapsed for the initial state; however, since their XAML is parsed and visual /logical trees built, it's very slow to show what amounts to an almost blank object.
It looks like (and would like confirmation here) that using a ContentControl with an initial state of Collapsed and then embedding the desired control as a DataTemplate for that ContentControl, will defer loading of the desired control in the DataTemplate until the ContentControl is made visible.
I've built a generic DeferredContentControl that listens for the LayoutUpdated event of the main UI control (in general whatever element it is that I want to appear quickly), and when the first LayoutUpdated event of that UIElement fires, I used the Dispatcher to flip the visibility of the DeferredContentControl to true, which causes the control in the DeferredContentControl's DataTemplate to instantiate. By the time the user has reacted to the initial view of the screen (which is now fast), the "slow to load" (but still collapsed) control in the data template is ready.
Does this seem like a sound approach? any pitfalls? It seems to work well in testing both for Silverlight and WPF, and while it doesn't make things any faster it gives the perception of being as much as 50% snappier in my specific scenario.
I had the same problem (in a Silverlight project), and solved it in nearly the same way. It proved to be working as expected, have not encountered any pitfalls yet.
When you need to control the point in time when xaml is parsed and view elements are instantiated you can always use DataTemplates (not necessarily in cunjuction with ContentControl). You can call DataTemplate.LoadContent() to instatiate it, you don't have to switch the visibility of a ContentControl (although internally this will result in such a LoadContent call).
Have a look at my implementation if you want, it can even display a static text message while the heavier VisualTree is build:
<DeferredContent HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<DeferredContent.DeferredContentTemplate>
<DataTemplate>
<MyHeavyView/>
</DataTemplate>
</Controls:DeferredContent.DeferredContentTemplate>
<TextBlock Text="Loading content..."/>
</Controls:DeferredContent>
and the code
public class DeferredContent : ContentPresenter
{
public DataTemplate DeferredContentTemplate
{
get { return (DataTemplate)GetValue(DeferredContentTemplateProperty); }
set { SetValue(DeferredContentTemplateProperty, value); }
}
public static readonly DependencyProperty DeferredContentTemplateProperty =
DependencyProperty.Register("DeferredContentTemplate",
typeof(DataTemplate), typeof(DeferredContent), null);
public DeferredContent()
{
Loaded += HandleLoaded;
}
private void HandleLoaded(object sender, RoutedEventArgs e)
{
Loaded -= HandleLoaded;
Deployment.Current.Dispatcher.BeginInvoke(ShowDeferredContent);
}
public void ShowDeferredContent()
{
if (DeferredContentTemplate != null)
{
Content = DeferredContentTemplate.LoadContent();
RaiseDeferredContentLoaded();
}
}
private void RaiseDeferredContentLoaded()
{
var handlers = DeferredContentLoaded;
if (handlers != null)
{
handlers( this, new RoutedEventArgs() );
}
}
public event EventHandler<RoutedEventArgs> DeferredContentLoaded;
}

C# WPF class property to label

I have the following class:
class MyTimer
{
class MyTimerInvalidType : SystemException
{
}
class MyTimerNegativeCycles : SystemException
{
}
private Timer timer = new Timer(1000);
private int cycles = 0;
public int Cycle
{
get
{
return this.cycles;
}
set
{
if(value >= 0)
this.cycles = value;
else
throw new MyTimerNegativeCycles();
}
}
private void timer_Tick(object sender, ElapsedEventArgs e)
{
try
{
this.Cycle--;
}
catch
{
this.Cycle = 0;
timer.Stop();
}
}
public MyTimer()
{
this.Cycle = 20;
timer.Elapsed += new ElapsedEventHandler(timer_Tick);
timer.Start();
}
}
In my MainWindow class I have a List I add a MyTimer to when a button is pressed:
private List<MyTimer> timers = new List<MyTimer>();
private void testbtn_Click(object sender, RoutedEventArgs e)
{
timers.Add(new MyTimer());
}
I tried to pass a label to the MyTimer class as a ref and update it but that won't work (can't access UI elements from another thread).
What is a good way to show the MyTimer.Cycle in a label so that it updates everytime the value is changed?
I must be able to "bind" each MyTimer to a different label from the code (or not bind it to a label at all).
You should use the BeginInvoke or Invoke method of the Dispatcher property of your label to change anything on your label or call any of it's methods:
private void timer_Tick(object sender, ElapsedEventArgs e)
{
try
{
this.Cycle--;
this.label.Dispatcher.BeginInvoke(new Action(
() => { label.Text = this.Cycle.ToString(); } ));
}
catch
{
this.Cycle = 0;
timer.Stop();
}
}
See Remarks section of the Dispatcher class or Dispatcher property.
The easiest solution to your problem is to use DispatchTimers. Dispatch timers use the windows message queue instead of a thread to dispatch timer tick events. This will make it so you don't have cross threading issues. Just keep in mind you are no longer working on a different thread and could lockup the UI if you do anything computationally expensive. Also due to the nature of dispatching on the message queue the timing is less accurate.
In WPF, you'd have a ViewModel (C#) associated with your View (XAML).
Read up on this if you're not familiar with MVVM.
Then the ViewModel would expose a property (let's call it Cycle) on which the View would bind:
<Label Content="{Binding Cycle}" />
Then if the value in the ViewModel has to be updated from another thread, do it like this:
Application.Current.Dispatcher.Invoke(new Action(() =>
{
//Update here
}));
That will execute the update logic on the UI thread.
If you're new to WPF I'd strongly suggest that read a bit about DataBinding and Data Templating.
To start, the simplest way do display windows data in older UI models (like Windows Forms) has always been to have code in the code-behind set some property of the UI. This has changed drastically with WPF and the goal now is to have the UI look at business objects (like your MyTimer) and set the UI accordingly.
First we need to expose your business objects to the xaml of your application.
Me.DataContext = new MyTimer();
This sets the data context for the Window/UserControl to be the a new MyTimer(); Because the DataContext property is automatically based from a parent UI element to a child UI elelement (unless the child defines it's own DataContext), every element in your Window/UserControl will now have a DataContext of this object.
Next we can create a binding to a property of this object. By default all bindings are relative to the DataContext of the control from which it's located.
<Label Content="{Binding Cycle}" />
So in the previous example the binding was on the content property of the label. So in this case it will automatically set the Content to the value of the "Cycle" property from the DataContext (MyTimer)!
There is however one catch. If you run this sample as is WPF will take the value when the form loads but it won't update the label ever again! The key here to updating the UI is to implement the INotifyPropertyChanged interface.
This interface simply tells any listeners whenever a property (such as your Cycles) changes. The great thing is that Bindings automatically support this interface and will automatically propagate changes when your source implements INotifyPropertyChanged.
public class MyTimer : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int cycles;
public int Cycles
{
get
{
return cycles;
}
set
{
if (cycles < 0)
{
throw new ArgumentOutOfRangeException("value", "Cycles cannot be set to a number smaller than 0.");
}
else if(value <> cycles)
{
cycles = value;
if (PropertyChanged != null)
{
PropertyChanged(Me, new PropertyChangedEventArgs("Cycles"))
}
}
}
}
//insert your constructor(s) and timer code here.
}
And voila! Your timer will now update the UI with it's cycles property.
You however also noted that you were storing your MyTimer objects in a list. If you were to instead put them inside an ObservableCollection (the default implementation of INotifyCollectionChanged - the collection variant of INotifyPropertyChanged) you can do other neat tricks:
In your Window/UserControl constructor:
ObservableCollection<MyTimer> timers = New ObservableCollection<MyTimer>();
timers.Add(New MyTimer());
DataContext = timers;
Then you can display them all at once in your xaml:
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label>
<TextBlock Text="{Binding StringFormat='Cycles Remaining: {0}'}" />
</Label>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

Silverlight MVVM and dealing with FOCUS

I'm developing complex data entry forms with various pop-up lookups, etc. Because of different things - focus of certain controls get lost and I need some way to set focus in MVVM. So far I came up with attached property which I coded like this(actual dependency property declaration skipped):
private static void SetFocus(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textBox = d as TextBox;
if (textBox != null)
{
textBox.Focus();
}
}
So, it's pretty simple. When property changes - focus get's set.
My view:
<TextBox Text="{Binding CurrentItem.SerialNumber, Mode=TwoWay, NotifyOnValidationError=True}"
behaviors:TextBoxBehaviors.IsFocused="{Binding SecondaryControlFocus}"
Grid.Column="1" Grid.Row="2" Margin="1" Grid.ColumnSpan="2" TabIndex="2" />
As you see - I attach that behavior and Bind to "SecondaryControlFocus" property.
ViewModel:
public bool SecondaryControlFocus
{
get
{
return this.secondaryControlFocus;
}
set
{
this.secondaryControlFocus = value;
this.RaisePropertyChanged(() => this.SecondaryControlFocus);
}
}
And code how I set focus:
this.SecondaryControlFocus = !this.SecondaryControlFocus;
To me this code smells because I have to toggle property force and back in order to trigger property..
Is there nicer way to accomplish what I do? There is nothing more irritating when power user can't use TAB keys... And I need to get control over focusing in MVVM, this is important for proper data entry flow. But I want code to be somewhat "nice"
It does smell, but I don't think there's anything we can do about it
Usually I do the same thing you have with the AttachedProperty, and keep a single IsFocused bool somewhere in the View (since this is a View-Specific problem, and should not be mixed in with the business logic). I'll then have the View listen to some kind of Event System such as (PRISM's EventAggregator or MVVM Light's Messenger) for ResetFocus events, and I'll raise the ResetFocus event whenever something causes focus to change between my windows/pages, or after a dialog box.
It's not pretty, but it works.

how to bind the click event of a button and the selecteditemchanged of a listbox to a viewmodel in mvvm in Silverlight

i'm just starting with the mvvm model in Silverlight.
In step 1 i got a listbox bound to my viewmodel, but now i want to propagate a click in a button and a selecteditemchanged of the listbox back to the viewmodel.
I guess i have to bind the click event of the button and the selecteditemchanged of the listbox to 2 methods in my viewmodel somehow?
For the selecteditemchanged of the listbox i think there must also be a 'return call' possible when the viewmodel tries to set the selecteditem to another value?
i come from a asp.net (mvc) background, but can't figure out how to do it in silverlight.
Roboblob provides excellent step-by-step solution for Silverlight 4. It strictly follows MVVM paradigm.
I would not bind or tie the VM in any way directly to the events of controls within the View. Instead, have a separate event that is raised by the View in response to the button click.
[disclaimer: this code is all done straight from my head, not copy & pasted from VS - treat it as an example!!]
So in pseudo code, the View will look like this:
private void MyView_Loaded(...)
{
MyButton.Click += new EventHandler(MyButton_Click);
}
private void MyButton_Click(...)
{
//Raise my event:
OnUserPressedGo();
}
private void OnUserPressedGo()
{
if (UserPressedTheGoButton != null)
this.UserPressedTheGoButton(this, EventArgs.Empty);
}
public EventHandler UserPressedTheGoButton;
and the VM would have a line like this:
MyView.UserPressedTheGoButton += new EventHandler(myHandler);
this may seem a little long-winded, why not do it a bit more directly? The main reason for this is you do not want to tie your VM too tightly (if at all) to the contents of the View, otherwise it becomes difficult to change the View. Having one UI agnostic event like this means the button can change at any time without affecting the VM - you could change it from a button to a hyperlink or that kool kat designer you hire may change it to something totally weird and funky, it doesn't matter.
Now, let's talk about the SelectedItemChanged event of the listbox. Chances are you want to intercept an event for this so that you can modify the data bound to another control in the View. If this is a correct assumption, then read on - if i'm wrong then stop reading and reuse the example from above :)
The odds are that you may be able to get away with not needing a handler for that event. If you bind the SelectedItem of the listbox to a property in the VM:
<ListBox ItemSource={Binding SomeList} SelectedItem={Binding MyListSelectedItem} />
and then in the MyListSelectedItem property of the VM:
public object MyListSelectedItem
{
get { return _myListSelectedItem; }
set
{
bool changed = _myListSelectedItem != value;
if (changed)
{
_myListSelectedItem = value;
OnPropertyChanged("MyListSelectedItem");
}
}
}
private void OnPropertyChanged(string propertyName)
{
if (this.NotifyPropertyChanged != null)
this.NotifyPropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
To get that NotifyPropertyChanged event, just implement the INotifyPropertyChanged interface on your VM (which you should have done already). That is the basic stuff out of the way... what you then follow this up with is a NotifyPropertyChanged event handler on the VM itself:
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "MyListSelectedItem":
//at this point i know the value of MyListSelectedItem has changed, so
//i can now retrieve its value and use it to modify a secondary
//piece of data:
MySecondaryList = AllAvailableItemsForSecondaryList.Select(p => p.Id == MyListSelectedItem.Id);
break;
}
}
All you need now is for MySecondaryList to also notify that its value has changed:
public List<someObject> MySecondaryList
{
get { return _mySecondaryList; }
set
{
bool changed = .......;
if (changed)
{
... etc ...
OnNotifyPropertyChanged("MySecondaryList");
}
}
}
and anything bound to it will automatically be updated. Once again, it may seem that this is the long way to do things, but it means you have avoided having any handlers for UI events from the View, you have kept the abstraction between the View and the ViewModel.
I hope this has made some sense to you. With my code, i try to have the ViewModel knowing absolutely zero about the View, and the View only knowing the bare minimum about the ViewModel (the View recieves the ViewModel as an interface, so it can only know what the interface has specified).
Regarding binding the button click event I can recommend Laurent Bugnion's MVVM Light Toolkit (http://www.galasoft.ch/mvvm/getstarted/) as a way of dealing with this, I'll provide a little example, but Laurent's documentation is most likely a better way of understanding his framework.
Reference a couple of assemblies in your xaml page
xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
add a blend behaviour to the button
<Button Content="Press Me">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<command:EventToCommand Command="{Binding ViewModelEventName}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
and create the event within your viewmodel which will be called when the button is clicked
public RelayCommand ViewModelEventName { get; protected set; }
...
public PageViewModel()
{
ViewModelEventName = new RelayCommand(
() => DoWork()
);
}
This supports passing parameters, checking whether execution is allowed etc also.
Although I haven't used it myself, I think the Prism framework also allows you to do something similar.

WPF: Cancel a user selection in a databound ListBox?

How do I cancel a user selection in a databound WPF ListBox? The source property is set correctly, but the ListBox selection is out of sync.
I have an MVVM app that needs to cancel a user selection in a WPF ListBox if certain validation conditions fail. Validation is triggered by a selection in the ListBox, rather than by a Submit button.
The ListBox.SelectedItem property is bound to a ViewModel.CurrentDocument property. If validation fails, the setter for the view model property exits without changing the property. So, the property to which ListBox.SelectedItem is bound doesn't get changed.
If that happens, the view model property setter does raise the PropertyChanged event before it exits, which I had assumed would be enough to reset the ListBox back to the old selection. But that's not working--the ListBox still shows the new user selection. I need to override that selection and get it back in sync with the source property.
Just in case that's not clear, here is an example: The ListBox has two items, Document1 and Document2; Document1 is selected. The user selects Document2, but Document1 fails to validate. The ViewModel.CurrentDocument property is still set to Document1, but the ListBox shows that Document2 is selected. I need to get the ListBox selection back to Document1.
Here is my ListBox Binding:
<ListBox
ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
I did try using a callback from the ViewModel (as an event) to the View (which subscribes to the event), to force the SelectedItem property back to the old selection. I pass the old Document with the event, and it is the correct one (the old selection), but the ListBox selection doesn't change back.
So, how do I get the ListBox selection back in sync with the view model property to which its SelectedItem property is bound? Thanks for your help.
For future stumblers on this question, this page is what ultimately worked for me:
http://blog.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box.aspx
It's for a combobox, but works for a listbox just fine, since in MVVM you don't really care what type of control is calling the setter. The glorious secret, as the author mentions, is to actually change the underlying value and then change it back. It was also important to run this “undo” on a separate dispatcher operation.
private Person _CurrentPersonCancellable;
public Person CurrentPersonCancellable
{
get
{
Debug.WriteLine("Getting CurrentPersonCancellable.");
return _CurrentPersonCancellable;
}
set
{
// Store the current value so that we can
// change it back if needed.
var origValue = _CurrentPersonCancellable;
// If the value hasn't changed, don't do anything.
if (value == _CurrentPersonCancellable)
return;
// Note that we actually change the value for now.
// This is necessary because WPF seems to query the
// value after the change. The combo box
// likes to know that the value did change.
_CurrentPersonCancellable = value;
if (
MessageBox.Show(
"Allow change of selected item?",
"Continue",
MessageBoxButton.YesNo
) != MessageBoxResult.Yes
)
{
Debug.WriteLine("Selection Cancelled.");
// change the value back, but do so after the
// UI has finished it's current context operation.
Application.Current.Dispatcher.BeginInvoke(
new Action(() =>
{
Debug.WriteLine(
"Dispatcher BeginInvoke " +
"Setting CurrentPersonCancellable."
);
// Do this against the underlying value so
// that we don't invoke the cancellation question again.
_CurrentPersonCancellable = origValue;
OnPropertyChanged("CurrentPersonCancellable");
}),
DispatcherPriority.ContextIdle,
null
);
// Exit early.
return;
}
// Normal path. Selection applied.
// Raise PropertyChanged on the field.
Debug.WriteLine("Selection applied.");
OnPropertyChanged("CurrentPersonCancellable");
}
}
Note: The author uses ContextIdle for the DispatcherPriority for the action to undo the change. While fine, this is a lower priority than Render, which means that the change will show in the UI as the selected item momentarily changing and changing back. Using a dispatcher priority of Normal or even Send (the highest priority) preempts the display of the change. This is what I ended up doing. See here for details about the DispatcherPriority enumeration.
In .NET 4.5 they added the Delay field to the Binding. If you set the delay it will automatically wait to update so there is no need for the Dispatcher in the ViewModel. This works for validation of all Selector elements like the ListBox's and ComboBox's SelectedItem properties. The Delay is in milliseconds.
<ListBox
ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, Delay=10}" />
-snip-
Well forget what I wrote above.
I just did an experiment, and indeed SelectedItem goes out of sync whenever you do anything more fancy in the setter. I guess you need to wait for the setter to return, and then change the property back in your ViewModel asynchronously.
Quick and dirty working solution (tested in my simple project) using MVVM Light helpers:
In your setter, to revert to previous value of CurrentDocument
var dp = DispatcherHelper.UIDispatcher;
if (dp != null)
dp.BeginInvoke(
(new Action(() => {
currentDocument = previousDocument;
RaisePropertyChanged("CurrentDocument");
})), DispatcherPriority.ContextIdle);
it basically queues the property change on the UI thread, ContextIdle priority will ensure it will wait for UI to be in consistent state. it Appears you cannot freely change dependency properties while inside event handlers in WPF.
Unfortunately it creates coupling between your view model and your view and it's an ugly hack.
To make DispatcherHelper.UIDispatcher work you need to do DispatcherHelper.Initialize() first.
Got it! I am going to accept majocha's answer, because his comment underneath his answer led me to the solution.
Here is wnat I did: I created a SelectionChanged event handler for the ListBox in code-behind. Yes, it's ugly, but it works. The code-behind also contains a module-level variable, m_OldSelectedIndex, which is initialized to -1. The SelectionChanged handler calls the ViewModel's Validate() method and gets a boolean back indicating whether the Document is valid. If the Document is valid, the handler sets m_OldSelectedIndex to the current ListBox.SelectedIndex and exits. If the document is invalid, the handler resets ListBox.SelectedIndex to m_OldSelectedIndex. Here is the code for the event handler:
private void OnSearchResultsBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewModel = (MainViewModel) this.DataContext;
if (viewModel.Validate() == null)
{
m_OldSelectedIndex = SearchResultsBox.SelectedIndex;
}
else
{
SearchResultsBox.SelectedIndex = m_OldSelectedIndex;
}
}
Note that there is a trick to this solution: You have to use the SelectedIndex property; it doesn't work with the SelectedItem property.
Thanks for your help majocha, and hopefully this will help somebody else down the road. Like me, six months from now, when I have forgotten this solution...
If you are serious about following MVVM and don't want any code behind, and also don't like the use of the Dispatcher, which frankly is not elegant either, the following solution works for me and is by far more elegant than most of the solutions provided here.
It is based on the notion that in code behind you are able to stop the selection using the SelectionChanged event. Well now, if this is the case, why not create a behavior for it, and associate a command with the SelectionChanged event. In the viewmodel you can then easily remember the previous selected index and the current selected index. The trick is to have binding to your viewmodel on SelectedIndex and just let that one change whenever the selection changes. But immediately after the selection really has changed, the SelectionChanged event fires which now is notified via the command to your viewmodel. Because you remember the previously selected index, you can validate it and if not correct, you move the selected index back to the original value.
The code for the behavior is as follows:
public class ListBoxSelectionChangedBehavior : Behavior<ListBox>
{
public static readonly DependencyProperty CommandProperty
= DependencyProperty.Register("Command",
typeof(ICommand),
typeof(ListBoxSelectionChangedBehavior),
new PropertyMetadata());
public static DependencyProperty CommandParameterProperty
= DependencyProperty.Register("CommandParameter",
typeof(object),
typeof(ListBoxSelectionChangedBehavior),
new PropertyMetadata(null));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
protected override void OnAttached()
{
AssociatedObject.SelectionChanged += ListBoxOnSelectionChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectionChanged -= ListBoxOnSelectionChanged;
}
private void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
Command.Execute(CommandParameter);
}
}
Using it in XAML:
<ListBox x:Name="ListBox"
Margin="2,0,2,2"
ItemsSource="{Binding Taken}"
ItemContainerStyle="{StaticResource ContainerStyle}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
HorizontalContentAlignment="Stretch"
SelectedIndex="{Binding SelectedTaskIndex, Mode=TwoWay}">
<i:Interaction.Behaviors>
<b:ListBoxSelectionChangedBehavior Command="{Binding SelectionChangedCommand}"/>
</i:Interaction.Behaviors>
</ListBox>
The code that is appropriate in the viewmodel is as follows:
public int SelectedTaskIndex
{
get { return _SelectedTaskIndex; }
set { SetProperty(ref _SelectedTaskIndex, value); }
}
private void SelectionChanged()
{
if (_OldSelectedTaskIndex >= 0 && _SelectedTaskIndex != _OldSelectedTaskIndex)
{
if (Taken[_OldSelectedTaskIndex].IsDirty)
{
SelectedTaskIndex = _OldSelectedTaskIndex;
}
}
else
{
_OldSelectedTaskIndex = _SelectedTaskIndex;
}
}
public RelayCommand SelectionChangedCommand { get; private set; }
In the constructor of the viewmodel:
SelectionChangedCommand = new RelayCommand(SelectionChanged);
RelayCommand is part of MVVM light. Google it if you don't know it.
You need to refer to
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
and hence you need to reference System.Windows.Interactivity.
I came up against this recently, and came up with a solution that works well with my MVVM, without the need for and code behind.
I created a SelectedIndex property in my model and bound the listbox SelectedIndex to it.
On the View CurrentChanging event, I do my validation, if it fails, I simply use the code
e.cancel = true;
//UserView is my ICollectionView that's bound to the listbox, that is currently changing
SelectedIndex = UserView.CurrentPosition;
//Use whatever similar notification method you use
NotifyPropertyChanged("SelectedIndex");
It seems to work perfectly ATM. There may be edge cases where it doesnt, but for now, it does exactly what I want.
I had a very similar problem, the difference being that I am using ListView bound to an ICollectionView and was using IsSynchronizedWithCurrentItem rather than binding the SelectedItem property of the ListView. This worked well for me until I wanted to cancel the CurrentItemChanged event of the underlying ICollectionView, which left the ListView.SelectedItem out of sync with the ICollectionView.CurrentItem.
The underlying problem here is keeping the view in sync with the view model. Obviously cancelling a selection change request in the view model is trivial. So we really just need a more responsive view as far as I'm concerned. I'd rather avoid putting kludges into my ViewModel to work around limitations of the ListView synchronization. On the other hand I'm more than happy to add some view-specific logic to my view code-behind.
So my solution was to wire my own synchronization for the ListView selection in the code-behind. Perfectly MVVM as far as I'm concerned and more robust than the default for ListView with IsSynchronizedWithCurrentItem.
Here is my code behind ... this allows changing the current item from the ViewModel as well. If the user clicks the list view and changes the selection, it will immediately change, then change back if something down-stream cancels the change (this is my desired behavior). Note I have IsSynchronizedWithCurrentItem set to false on the ListView. Also note that I am using async/await here which plays nicely, but requires a little double-checking that when the await returns, we are still in the same data context.
void DataContextChangedHandler(object sender, DependencyPropertyChangedEventArgs e)
{
vm = DataContext as ViewModel;
if (vm != null)
vm.Items.CurrentChanged += Items_CurrentChanged;
}
private async void myListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var vm = DataContext as ViewModel; //for closure before await
if (vm != null)
{
if (myListView.SelectedIndex != vm.Items.CurrentPosition)
{
var changed = await vm.TrySetCurrentItemAsync(myListView.SelectedIndex);
if (!changed && vm == DataContext)
{
myListView.SelectedIndex = vm.Items.CurrentPosition; //reset index
}
}
}
}
void Items_CurrentChanged(object sender, EventArgs e)
{
var vm = DataContext as ViewModel;
if (vm != null)
myListView.SelectedIndex = vm.Items.CurrentPosition;
}
Then in my ViewModel class I have ICollectionView named Items and this method (a simplified version is presented).
public async Task<bool> TrySetCurrentItemAsync(int newIndex)
{
DataModels.BatchItem newCurrentItem = null;
if (newIndex >= 0 && newIndex < Items.Count)
{
newCurrentItem = Items.GetItemAt(newIndex) as DataModels.BatchItem;
}
var closingItem = Items.CurrentItem as DataModels.BatchItem;
if (closingItem != null)
{
if (newCurrentItem != null && closingItem == newCurrentItem)
return true; //no-op change complete
var closed = await closingItem.TryCloseAsync();
if (!closed)
return false; //user said don't change
}
Items.MoveCurrentTo(newCurrentItem);
return true;
}
The implementation of TryCloseAsync could use some kind of dialog service to elicit a close confirmation from the user.
Bind ListBox's property: IsEnabled="{Binding Path=Valid, Mode=OneWay}" where Valid is the view-model property with the validation algoritm. Other solutions look too far-fetched in my eyes.
When the disabled appearance is not allowed, a style could help out, but probably the disabled style is ok because changing the selection is not allowed.
Maybe in .NET version 4.5 INotifyDataErrorInfo helps, I dont'know.

Resources