I seem to have reached some sort of MVVM breaking point here.
I would like for a control to have its opacity animated for half a second (DoubleAnimation from 0.5 to 1.0) when the underlying view model object have its "Status" property changed. I achieved this at first using a DataTrigger but since I haven't found a way to react to ANY change, just a given value, I had to always flip the VM objects "Status" property to a special "pending" value before setting it to its intended value. (Is there a way to react to ANY change btw?)
This was hacky so I started fiddling with EventTriggers instead...
This is what I've tried so far:
Using a normal EventTrigger
This seems to require a RoutedEvent but that, in turn, requires that my underlying view model object inherits from DependencyObject.
Using i:Interaction.Triggers
That way I can listen to and react to normal .NET events but I haven't found a way to start a StoryBoard using that approach.
Using i:Interaction.Triggers and writing a Behavior
This experiment fell short on the fact I found no way to attach my custom behavior to its associated control.
This is what the XAML looked like:
<cc:MyControl>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Updated">
<i:Interaction.Behaviors>
<cv:OpacityBehavior Duration="0:0:0:5" />
</i:Interaction.Behaviors>
</i:EventTrigger>
</i:Interaction.Triggers>
And here's the custom behavior:
class OpacityBehavior : Behavior<MyControl>
{
public Duration Duration { get; set; }
protected override void OnAttached()
{
base.OnAttached();
var animation = new DoubleAnimation(0.5, 1, Duration, FillBehavior.HoldEnd);
var associatedObject = lookupVisualParent(this);
associatedObject.BeginAnimation(UIElement.OpacityProperty, animation);
}
}
That didn't work because the XAML parser required it to be attached directly to "MyControl" but I need to attach it to the event trigger. I then tried this approach:
class OpacityBehavior : Behavior<DependencyObject>
{
public Duration Duration { get; set; }
protected override void OnAttached()
{
base.OnAttached();
var animation = new DoubleAnimation(0.5, 1, Duration, FillBehavior.HoldEnd);
var associatedObject = lookupVisualParent(this);
associatedObject.BeginAnimation(UIElement.OpacityProperty, animation);
}
private UIElement lookupVisualParent(DependencyObject dObj)
{
if (dObj is UIElement)
return (UIElement) dObj;
if (dObj == null)
return null;
return lookupVisualParent(LogicalTreeHelper.GetParent(dObj));
}
}
This failed on the fact that lookupVisualParent doesn't work. The logical parent of the behavior is always null.
It strikes me this should be a fairly common task? Is there a good solution to this problem? I find it strange that I will have write my view model classes so that they derive from DependencyObject in order to start an animation when an event fires.
Cheers
You could simply use a flag: set a flag on your VM called 'RecentlyChangedFlag'. Pulse it to true, then false, whenever the appropriate value(s) change. You could do that like this:
private bool _changedFlag;
public bool ChangedFlag
{
get
{
if (_changedFlag)
{
_changedFlag = false;
OnPropertyChanged("ChangedFlag");
return true;
}
// (else...)
return false;
}
protected set
{
_changedFlag = value;
OnPropertyChanged("ChangedFlag");
}
}
I.e., with the above code set ChangedFlag = true when you want to signal the animation to start. It will be reset to false after WPF queries the true value.
Then have the animation occur when the value of RecentlyChangedFlag is true, as an EnterAction for instance.
Hope that helps.
Related
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>
I understand that ViewModel shouldn't have any knowledge of View, but how can I call MediaElement.Play() method from ViewModel, other than having a reference to View (or directly to MediaElement) in ViewModel?
Other (linked) question: how can I manage View's controls visibility from ViewModel without violating MVVM pattern?
1) Do not call Play() from the view model. Raise an event in the view model instead (for instance PlayRequested) and listen to this event in the view:
view model:
public event EventHandler PlayRequested;
...
if (this.PlayRequested != null)
{
this.PlayRequested(this, EventArgs.Empty);
}
view:
ViewModel vm = new ViewModel();
this.DataContext = vm;
vm.PlayRequested += (sender, e) =>
{
this.myMediaElement.Play();
};
2) You can expose in the view model a public boolean property, and bind the Visibility property of your controls to this property. As Visibility is of type Visibility and not bool, you'll have to use a converter.
You can find a basic implementation of such a converter here.
This related question might help you too.
For all the late-comers,
There are many ways to achieve the same result and it really depends on how you would like to implement yours, as long as your code is not difficult to maintain, I do believe it's ok to break the MVVM pattern under certain cases.
But having said that, I also believe there is always way to do this within the pattern, and the following is one of them just in case if anyone would like to know what other alternatives are available.
The Tasks:
we don't want to have direct reference from the ViewModel to any UI elements, i.e. the the MediaElement and the View itself.
we want to use Command to do the magic here
The Solution:
In short, we are going to introduce an interface between the View and the ViewModel to break the dependecy, and the View will be implementing the interface and be responsible for the direct controlling of the MediaElement while leaving the ViewModel talking only to the interface, which can be swapped with other implementation for testing purposes if needed, and here comes the long version:
Introduce an interface called IMediaService as below:
public interface IMediaService
{
void Play();
void Pause();
void Stop();
void Rewind();
void FastForward();
}
Implement the IMediaService in the View:
public partial class DemoView : UserControl, IMediaService
{
public DemoView()
{
InitializeComponent();
}
void IMediaService.FastForward()
{
this.MediaPlayer.Position += TimeSpan.FromSeconds(10);
}
void IMediaService.Pause()
{
this.MediaPlayer.Pause();
}
void IMediaService.Play()
{
this.MediaPlayer.Play();
}
void IMediaService.Rewind()
{
this.MediaPlayer.Position -= TimeSpan.FromSeconds(10);
}
void IMediaService.Stop()
{
this.MediaPlayer.Stop();
}
}
we then do few things in the DemoView.XAML:
Give the MediaElement a name so the code behind can access it like above:
<MediaElement Source="{Binding CurrentMedia}" x:Name="MediaPlayer"/>
Give the view a name so we can pass it as a parameter, and
import the interactivity namespace for later use (some default namespaces are omitted for simplicity reason):
<UserControl x:Class="Test.DemoView"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ia="http://schemas.microsoft.com/expression/2010/interactivity"
x:Name="MediaService">
Hookup the Loaded event through Trigger to pass the view itself to the view model through a Command
<ia:Interaction.Triggers>
<ia:EventTrigger EventName="Loaded">
<ia:InvokeCommandAction Command="{Binding LoadedCommand}" CommandParameter="{Binding ElementName=MediaService}"></ia:InvokeCommandAction>
</ia:EventTrigger>
</ia:Interaction.Triggers>
last but not least, we need to hookup the media controls through Commands:
<Button Command="{Binding PlayCommand}" Content="Play"></Button>
<Button Command="{Binding PauseCommand}" Content="Pause"></Button>
<Button Command="{Binding StopCommand}" Content="Stop"></Button>
<Button Command="{Binding RewindCommand}" Content="Rewind"></Button>
<Button Command="{Binding FastForwardCommand}" Content="FastForward"></Button>
We now can catch everything in the ViewModel (I'm using prism's DelegateCommand here):
public class AboutUsViewModel : SkinTalkViewModelBase, IConfirmNavigationRequest
{
public IMediaService {get; private set;}
private DelegateCommand<IMediaService> loadedCommand;
public DelegateCommand<IMediaService> LoadedCommand
{
get
{
if (this.loadedCommand == null)
{
this.loadedCommand = new DelegateCommand<IMediaService>((mediaService) =>
{
this.MediaService = mediaService;
});
}
return loadedCommand;
}
}
private DelegateCommand playCommand;
public DelegateCommand PlayCommand
{
get
{
if (this.playCommand == null)
{
this.playCommand = new DelegateCommand(() =>
{
this.MediaService.Play();
});
}
return playCommand;
}
}
.
. // other commands are not listed, but you get the idea
.
}
Side note: I use Prism's Auto Wiring feature to link up the View and ViewModel. So at the View's code behind file there is no DataContext assignment code, and I prefer to keep it that way, and hence I chose to use purely Commands to achieve this result.
I use media element to play sounds in UI whenever an event occurs in the application. The view model handling this, was created with a Source property of type Uri (with notify property changed, but you already know you need that to notify UI).
All you have to do whenever source changes (and this is up to you), is to set the source property to null (this is why Source property should be Uri and not string, MediaElement will naturally throw exception, NotSupportedException I think), then set it to whatever URI you want.
Probably, the most important aspect of this tip is that you have to set MediaElement's property LoadedBehaviour to Play in XAML of your view. Hopefully no code behind is needed for what you want to achieve.
The trick is extremely simple so I won't post a complete example. The view model's play function should look like this:
private void PlaySomething(string fileUri)
{
if (string.IsNullOrWhiteSpace(fileUri))
return;
// HACK for MediaElement: to force it to play a new source, set source to null then put the real source URI.
this.Source = null;
this.Source = new Uri(fileUri);
}
Here is the Source property, nothing special about it:
#region Source property
/// <summary>
/// Stores Source value.
/// </summary>
private Uri _Source = null;
/// <summary>
/// Gets or sets file URI to play.
/// </summary>
public Uri Source
{
get { return this._Source; }
private set
{
if (this._Source != value)
{
this._Source = value;
this.RaisePropertyChanged("Source");
}
}
}
#endregion Source property
As for Visibility, and stuff like this, you can use converters (e.g. from bool to visibility, which you can find on CodePlex for WPF, SL, WP7,8) and bind your control's property to that of the view model's (e.g. IsVisible). This way, you control parts of you view's aspect. Or you can just have Visibility property typed System.Windows.Visibility on your view model (I don't see any pattern breach here). Really, it's not that uncommon.
Good luck,
Andrei
P.S. I have to mention that .NET 4.5 is the version where I tested this, but I think it should work on other versions as well.
I have control in SL4. I want data validation on button click. Big problem is normally SL4 give validation using binding property.
like example given shown in this example
http://weblogs.asp.net/dwahlin/archive/2010/08/15/validating-data-in-silverlight-4-applications-idataerrorinfo.aspx
<TextBox Text="{Binding Name,Mode=TwoWay,ValidatesOnDataErrors=true}"
Height="23"
Width="120"
HorizontalAlignment="Left"
VerticalAlignment="Top" />
BUT I WANT TO SHOW ERROR MESSAGE LIKE THIS ....
using my own code like on button click i check
(textbox1.text == null ) then set this style of error to textbox1
One way of deferring validation is to set the property UpdateSourceTrigger=Explicit in the bindings. If you do this, the bindings won't update the source objects, and hence won't cause validation errors, until you explicitly tell the bindings to do so. When your button is clicked, you force an update on the bindings, using a line such as the following for each control:
someTextBox.GetBindingExpression(TextBox.TextProperty).UpdateSource();
You then have your property setters throwing exceptions for invalid data.
This approach can be a bit of a pain if there are quite a lot of controls to force binding updates on.
Also, forcing an update on the bindings has to be done in the code-behind of a control. If you're using a Command with the button as well then you might run in to an issue. Buttons can have both a Command and a Click event handler, and both will execute when the button is clicked on, but I don't know the order in which this happens or even if an order can be guaranteed. A quick experiment suggested that the event handler was executed before the command, but I don't know whether this is undefined behaviour. There is therefore the chance that the command will be fired before the bindings have been updated.
An approach to programmaticaly creating validation tooltips is to bind another property of the textbox and then deliberately cause an error with this binding.
'sapient' posted a complete solution, including code on the Silverlight forums (search for the post dated 07-08-2009 4:56 PM). In short, he/she creates a helper object with a property whose getter throws an exception, binds the Tag property of the textbox to this helper object and then forces an update on the binding.
'sapient's code was written before Silverlight 4 was released. We'll 'upgrade' his/her code to Silverlight 4. The class ControlValidationHelper becomes the following:
public class ControlValidationHelper : IDataErrorInfo
{
public string Message { get; set; }
public object ValidationError { get; set; }
public string Error
{
get { throw new NotImplementedException(); }
}
public string this[string columnName]
{
get { return Message; }
}
}
It's easy enough to knock up a quick demo application to try this out. I created the following three controls:
<TextBox x:Name="tbx" Text="{Binding Path=Text, ValidatesOnDataErrors=True, NotifyOnValidationError=True, Mode=TwoWay}" />
<Button Click="ForceError_Click">Force error</Button>
<Button Click="ClearError_Click">Clear error</Button>
The Text property and the event handlers for the two buttons live in the code-behind and are as follows:
public string Text { get; set; }
private void ForceError_Click(object sender, RoutedEventArgs e)
{
var helper = new ControlValidationHelper() { Message = "oh no!" };
tbx.SetBinding(Control.TagProperty, new Binding("ValidationError")
{
Mode = BindingMode.TwoWay,
NotifyOnValidationError = true,
ValidatesOnDataErrors = true,
UpdateSourceTrigger = UpdateSourceTrigger.Explicit,
Source = helper
});
tbx.GetBindingExpression(Control.TagProperty).UpdateSource();
}
private void ClearError_Click(object sender, RoutedEventArgs e)
{
BindingExpression b = tbx.GetBindingExpression(Control.TagProperty);
if (b != null)
{
((ControlValidationHelper)b.DataItem).Message = null;
b.UpdateSource();
}
}
The 'Force error' button should make a validation error appear on the textbox, and the 'Clear error' button should make it go away.
One potential downside of this approach occurs if you are using a ValidationSummary. The ValidationSummary will list all validation errors against ValidationError instead of against the name of each property.
Although my answer wasn't regarded as preferable, I'm still sure that the MVVM pattern is the best choice to perform validation.
In my code you should use the model validator from this post about validation and any mvvm framework, for example MVVM Light.
It is much easier to add validation rules using the view model and model validator classes:
public class PersonViewModel : ViewModelBase, INotifyDataErrorInfo
{
private ModelValidator _validator = new ModelValidator();
public PersonViewModel()
{
this._validator.AddValidationFor(() => this.Age)
.Must(() => this.Age > 0)
.Show("Age must be greater than zero");
}
}
And you can validate the model if and only if a user explicitly clicks a button:
#region INotifyDataErrorInfo
public IEnumerable GetErrors(string propertyName)
{
return this._validator.GetErrors(propertyName);
}
public bool HasErrors
{
get { return this._validator.ErrorMessages.Count > 0; }
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged = delegate { };
protected void OnErrorsChanged(string propertyName)
{
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
this.RaisePropertyChanged("HasErrors");
}
#endregion
public bool Validate()
{
var result = this._validator.ValidateAll();
this._validator.PropertyNames.ForEach(OnErrorsChanged);
return result;
}
As everyone can see, there is nothing difficult here, just 20-30 lines of code.
Moreover, the MVVM approach is much more flexible and you can reuse some common validation scenaries among several view models.
I'm wondering how to maintain the aspect ratio (i.e.: 16x9) of a window in WPF upon resize--if possible in a way that leverages MVVM. As I'm new to both MVVM and WPF, I'm not sure where to begin. Thanks.
This may be difficult to do with a "pure" MVVM implementation, because you need to know which direction the resize happened (horizontally or vertically). Note that if both change at once (i.e. the user resizes by dragging the corner), you will need to decide which of these to use.
In your ViewModel, you will probably have a property named AspectRatio.
In your View, you will most likely override the OnRenderSizeChanged event. Its then a matter of taste whether you do the work in the view using the property from the ViewModel, or whether you pass the value to a property in the ViewModel to do the work, and bind to the new values.
Example 1: Do the work here
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
if (sizeInfo.WidthChanged)
{
this.Width = sizeInfo.NewSize.Height * mViewModel.AspectRatio;
}
else
{
this.Height = sizeInfo.NewSize.Width * mViewModel.AspectRatio;
}
}
Example 2: Do the work in the ViewModel
View.xaml.cs
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
if (sizeInfo.WidthChanged)
{
viewModel.AspectWidth = sizeInfo.NewSize.Width;
}
else
{
viewModel.AspectHeight = sizeInfo.NewSize.Height;
}
}
ViewModel.cs
public Double AspectWidth
{
get { return mAspectWidth; }
set
{
// Some method that sets your property and implements INotifyPropertyChanged
SetValue("AspectWidth", ref mAspectWidth, value);
SetValue("AspectHeight", ref mAspectHeight, mAspectWidth * mAspectRatio);
}
}
public Double AspectHeight
{
get { return mAspectHeight; }
set
{
// Some method that sets your property and implements INotifyPropertyChanged
SetValue("AspectHeight", ref mAspectHeight, value);
SetValue("AspectWidth", ref mAspectWidth, mAspectHeight* mAspectRatio);
}
}
And your view (for example 2) would bind the window's width and height to the AspectWidth and AspectHeight properties in the viewmodel.
View.xaml
<Window Width="{Binding AspectWidth}"
Height="{Binding AspectHeight}">
</Window>
So, in either case, you override OnRenderSizeChanged. The details on how you implement that method are up to your tastes. I guess that Example #2 is closer to pure "MVVM" but it may also be overkill in this case.
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.