I have an error "Must create DependencySource on same Thread as the DependencyObject" in my project.
My comment is used to load a file and create a list. This list is bind to a ListBox. AL was working good. But I created a Task to load (load can be long). Now I have this error. I don't understand why it occurs.
There is my code :
MainView.xaml:
<ListBox ItemsSource="{Binding Results}"
SelectedItem="{Binding SelectedItem}">
<ListBox.InputBindings>
<KeyBinding Command="{Binding RemoveCommand}"
Key="Delete"/>
</ListBox.InputBindings>
</ListBox>
<Button Grid.Row="1" Grid.Column="0"
Style="{StaticResource StyleButton}"
Command="{Binding LoadCommand}"
Content="Open result"/>
MainViewModel:
#region Fields/Properties
public ImageWithPoints SelectedItem
{
get
{
return _selectedItem;
}
set
{
_selectedItem = value;
SelectedPointIndex = 1;
OnPropertyChanged();
OnPropertyChanged("Picture");
UpdatePoints();
}
}
public List<ImageWithPoints> Results
{
get
{
return _results;
}
set
{
_results = value;
if (value == null)
{
SelectedPointIndex = 0;
}
OnPropertyChanged();
}
}
public BitmapSource Picture
{
get
{
return SelectedItem?.Picture;
}
}
#endregion
#region Load
private ICommand _loadCommand;
public ICommand LoadCommand
{
get
{
if (_loadCommand == null)
_loadCommand = new RelayCommand(OnLoad, CanLoad);
return _loadCommand;
}
}
public void OnLoad()
{
StartRunning(this, null);
Task loadTask = new Task(new Action(() =>
{
Load();
Application.Current.Dispatcher.Invoke(new Action(() =>
{
StopRunning(this, null);
}));
}));
loadTask.Start();
}
public bool CanLoad()
{
return !IsRunning;
}
#endregion
#region Events
public event EventHandler OnStartRunning;
public event EventHandler OnStopRunning;
private void StartRunning(object sender, EventArgs e)
{
OnStartRunning(sender, e);
}
private void StopRunning(object sender, EventArgs e)
{
OnStopRunning(sender, e);
}
#enregion
#region Methods
public void Load()
{
// Open File
// Set to list
List<ImageWithPoints> listRes;
Results = listRes;
SelectedItem = Results[0];
}
#endregion
When I remove the line SelectedItem = Results[0]; I have no error (but application don't work has it should).
Set the SelectedItem property back on the UI thread once the Task has finished:
public void OnLoad()
{
StartRunning(this, null);
Task.Factory.StartNew(new Action(() =>
{
Load();
})).ContinueWith(task =>
{
SelectedItem = Results[0];
StopRunning(this, null);
}, System.Threading.CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
}
You can only access a UI element on the thread on which it was originally created so if your UpdatePoints() method accesses any control you must call this method on the UI thread.
Related
When I expand TreeViewItem with ~3000 items, application will be stuck for about 5 seconds. After searching for solution, I realised that write a custom virtualizing panel is the only way but is difficult for me.
Here is xaml I'm using (simplified):
<controls:FileMapPresenter ItemsSource="{Binding RootSource, Mode=OneWay}"
ItemTemplateSelector="{StaticResource templateSelector}">
<controls:FileMapPresenter.Resources>
<DataTemplate x:Key="folder_template">
<controls:FolderDataPersenter ItemsSource="{Binding Source, Mode=OneWay}"
ItemTemplateSelector="{StaticResource templateSelector}">
<controls:FolderDataPersenter.Header>
<TextBlock Text="{Binding Name, Mode=OneWay}" />
</controls:FolderDataPersenter.Header>
</controls:FolderDataPersenter>
</DataTemplate>
<DataTemplate x:Key="file_template">
<TextBlock Text="{Binding Name, Mode=OneWay}" />
</DataTemplate>
</controls:FileMapPresenter.Resources>
</controls:FileMapPresenter>
controls:FileMapPresenter is derived from ItemsControl, and controls:FolderDataPersenter is derived from TreeViewItem.
FolderDataModel (simplified):
public class FolderData : ViewModelBase
{
public FolderData(string name)
{
this.name = name;
}
private List<FileData> files = new List<FileData>();
public List<FileData> Files
{
get { return this.files; }
}
private List<FolderData> subFolders = new List<FolderData>();
public List<FolderData> SubFolders
{
get { return this.subFolders; }
}
private string name;
public string Name
{
get { return this.name; }
}
private AutoInvokeObservableCollection<object> source =
new AutoInvokeObservableCollection<object>();
/// <summary>
/// ObservableCollection which contains all the files and subFolders
/// </summary>
public AutoInvokeObservableCollection<object> Source
{
get { return this.source; }
}
}
FileDataModel (simplified):
public class FileData
{
public FileData(string name)
{
this.name = name;
}
private string name;
public string Name
{
get { return this.name; }
}
}
in controls:FolderDataPersenter, I try to cancel expanding call and put it into asynchronous, then add a expanding progress reporter to notify UI:
public class FolderDataPersenter : TreeViewItem
{
public FolderDataPersenter()
{
VirtualizingStackPanel.SetIsVirtualizing(this, true);
VirtualizingStackPanel.SetVirtualizationMode(this, VirtualizationMode.Recycling);
this.Dispatcher.BeginInvoke(
(Action)this.setExpandingEventListener,
System.Windows.Threading.DispatcherPriority.Loaded);
}
private void setExpandingEventListener()
{
if (this.Template != null)
{
var expander = this.Template.FindName("Expander", this) as ToggleButton;
if (expander != null)
{
expander.PreviewMouseLeftButtonDown += this.onExpanderPreviewMouseLeftButtonDown;
}
}
}
private void onExpanderPreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
e.Handled = true;
this.Dispatcher.BeginInvoke((Action)delegate
{
this.IsExpanded = !this.IsExpanded; // no work here
}, System.Windows.Threading.DispatcherPriority.Background);
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Business.TreeViewExpandingCounter.StepForward();
}
protected override void OnExpanded(RoutedEventArgs e)
{
var folder = this.DataContext as Models.FolderData;
if (folder != null)
{
if (folder.SubFolders.Count > 100)
{
Business.TreeViewExpandingCounter.Finish += this.onExpandingCounterDone;
Business.TreeViewExpandingCounter.Set(folder.SubFolders.Count);
}
}
base.OnExpanded(e);
}
private void onExpandingCounterDone()
{
Business.TreeViewExpandingCounter.Finish -= this.onExpandingCounterDone;
}
}
expanding progress reporter worked well but expanding asynchronously failed, application still stuck while expanding node.
so it seems I have returned to the first question "how to expand TreeViewItem faster?" which I have searched for half a day...
any suggestion?
What is the best way to use commands in WPF ?
I use some commands, thoses commands can take a time to execute. I want that my application not freeze while running but I want the features to be disabled.
there is my MainWindow.xaml :
<Window ...>
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Button Grid.Row="0"
Grid.Column="0"
Style="{StaticResource StyleButton}"
Content="Load"
Command="{Binding LoadCommand}"/>
<Button Grid.Row="0"
Grid.Column="1"
Style="{StaticResource StyleButton}"
Content="Generate"
Command="{Binding GenerateCommand}"/>
</Grid>
</Window>
and my MainViewModel.cs :
public class MainViewModel : ViewModelBase
{
#region GenerateCommand
#endregion
#region Load command
private ICommand _loadCommand;
public ICommand LoadCommand
{
get
{
if (_loadCommand == null)
_loadCommand = new RelayCommand(OnLoad, CanLoad);
return _loadCommand;
}
}
private void OnLoad()
{
//My code
}
private bool CanLoad()
{
return true;
}
#endregion
}
I saw a solution with background worker but I don't know how to use it. And I wonder if I should create one instance by command.
Is there a cleaner/best way ?
I want that my application not freeze while running but I want the features to be disabled.
The key to prevent the application from freezing is to perform any long-running operation on a background thread. The easiest way to do this is to start a Task. To disable the window you could bind its IsEnabled property to a source property of the view model that you set prior to starting the task. The following sample code should give you the idea:
public class MainViewModel : ViewModelBase
{
private RelayCommand _loadCommand;
public ICommand LoadCommand
{
get
{
if (_loadCommand == null)
_loadCommand = new RelayCommand(OnLoad, CanLoad);
return _loadCommand;
}
}
private void OnLoad()
{
IsEnabled = false;
_canLoad = false;
_loadCommand.RaiseCanExecuteChanged();
Task.Factory.StartNew(()=> { System.Threading.Thread.Sleep(5000); }) //simulate som long-running operation that runs on a background thread...
.ContinueWith(task =>
{
//reset the properties back on the UI thread once the task has finished
IsEnabled = true;
_canLoad = true;
}, System.Threading.CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
}
private bool _canLoad = true;
private bool CanLoad()
{
return _canLoad;
}
private bool _isEnabled;
public bool IsEnabled
{
get { return _isEnabled; }
set { _isEnabled = value; RaisePropertyChanged(); }
}
}
Note that you cannot access any UI element from a background thread since controls have thread affinity: http://volatileread.com/Thread/Index?id=1056
My approach to avoid UI freezing in these scenarios is to use async/await in the ICommand execution, and execute the long-running code on a background thread. Your modified code would look something like this:
public ICommand LoadCommand
{
get
{
if (_loadCommand == null)
_loadCommand = new RelayCommand(async o => await OnLoadAsync(), CanLoad);
return _loadCommand;
}
}
private async Task OnLoadAsync()
{
await Task.Run(() => MyLongRunningProcess());
}
If that background task needs to update anything bound to the UI then it needs to be wrapped in a Dispatcher.Invoke (or Dispatcher.BeginInvoke).
If you want to prevent the command from being executed a second time just set "CanLoad" to true before the await Task.Run(... line, and back to false after it.
I'd suggest to use Akka.Net: you can find an example with WPF on github.
I've forked it to impement stop and start commands:
my goal was to show bidirectional communication between Akka.Net actors and ViewModel.
You'll find the ViewModel calling the ActorSystem like this
private void StartCpuMethod() {
Debug.WriteLine("StartCpuMethod");
ActorSystemReference.Start();
}
private void StopCpuMethod() {
Debug.WriteLine("StopCpuMethod");
ActorSystemReference.Stop();
}
with an Actor receiving those messages
public CPUReadActor()
{
Receive<ReadCPURequestMessage>(msg => ReceiveReadDataMessage());
Receive<ReadCPUSyncMessage>(msg => ReceiveSyncMessage(msg));
}
private void ReceiveSyncMessage(ReadCPUSyncMessage msg)
{
switch (msg.Op)
{
case SyncOp.Start:
OnCommandStart();
break;
case SyncOp.Stop:
OnCommandStop();
break;
default:
throw new Exception("unknown Op " + msg.Op.ToString());
}
}
and the other way round from an Actor
public ChartingActor(Action<float, DateTime> dataPointSetter)
{
this._dataPointSetter = dataPointSetter;
Receive<DrawPointMessage>(msg => ReceiveDrawPointMessage(msg));
}
private void ReceiveDrawPointMessage(DrawPointMessage msg)
{
_dataPointSetter(msg.Value, msg.Date);
}
to the ViewModel
public MainWindowViewModel()
{
StartCpuCommand = new RelayCommand(StartCpuMethod);
StopCpuCommand = new RelayCommand(StopCpuMethod);
SetupChartModel();
Action<float, DateTime> dataPointSetter = new Action<float, DateTime>((v, d) => SetDataPoint(v, d));
ActorSystemReference.CreateActorSystem(dataPointSetter);
}
private void SetDataPoint(float value, DateTime date)
{
CurrentValue = value;
UpdateLineSeries(value, date);
}
The best way here it's a use of async/await, in my opinion. https://msdn.microsoft.com/ru-ru/library/mt674882.aspx
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
LoadCommand = new RelayCommand(async ol => await OnLoadAsync(), CanLoad);
}
public ICommand LoadCommand { get; }
private async void OnLoadAync()
{
await SomethingAwaitable();
}
private Task<bool> SomethingAwaitable()
{
//Your code
}
}
How can I cancel exiting from particular form after Cancel button (or X at the top right corner, or Esc) was clicked?
WPF:
<Window
...
x:Class="MyApp.MyView"
...
/>
<Button Content="Cancel" Command="{Binding CancelCommand}" IsCancel="True"/>
</Window>
ViewModel:
public class MyViewModel : Screen {
private CancelCommand cancelCommand;
public CancelCommand CancelCommand {
get { return cancelCommand; }
}
public MyViewModel() {
cancelCommand = new CancelCommand(this);
}
}
public class CancelCommand : ICommand {
public CancelCommand(MyViewModel viewModel) {
this.viewModel = viewModel;
}
public override void Execute(object parameter) {
if (true) { // here is a real condition
MessageBoxResult messageBoxResult = System.Windows.MessageBox.Show(
"Really close?", "Warning",
System.Windows.MessageBoxButton.YesNo);
if (messageBoxResult == MessageBoxResult.No) { return; }
}
viewModel.TryClose(false);
}
public override bool CanExecute(object parameter) {
return true;
}
}
Current code doesn't work. I want user to stay on current form if it chooses 'No' in popup dialog.
Also, overriding CanExecute doesn't help. It just disables the button. I want to allow user to hit the button, but then notify him/her, that data will be lost.
Maybe I should assign an event listener on button?
EDIT:
I managed showing popup on Cancel button. But I still can't manage Esc or X button (top right). It seems I was confused with Cancel button, because Execute method is executed when I click X button or Esc.
EDIT2:
I changed the question. It was 'how cancel Cancel button'. However, it wasn't what I was looking for. I need to cancel Esc or X button.
In 'MyViewModel' I add:
protected override void OnViewAttached(object view, object context) {
base.OnViewAttached(view, context);
(view as MyView).Closing += MyViewModel_Closing;
}
void MyViewModel_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
if (true) {
MessageBoxResult messageBoxResult = System.Windows.MessageBox.Show(
"Really close?", "Warning",
System.Windows.MessageBoxButton.YesNo);
if (messageBoxResult == MessageBoxResult.No) {
e.Cancel = true;
}
}
}
This solved my problem. However, I need ICommand to understand, which button was clicked, Save or Cancel. Is there any way to eliminate usage of event?
You are trying to do View's work in ViewModel class. Let your View class to handle the closing request and whether it should be canceled or not.
To cancel closing of a window you can subscribe to the Closing event of view and set CancelEventArgs.Cancel to true after showing a MessageBox.
Here is an example:
<Window
...
x:Class="MyApp.MyView"
Closing="OnClosing"
...
/>
</Window>
Code behind:
private void OnClosing(object sender, CancelEventArgs e)
{
var result = MessageBox.Show("Really close?", "Warning", MessageBoxButton.YesNo);
if (result != MessageBoxResult.Yes)
{
e.Cancel = true;
}
// OR, if triggering dialog via view-model:
bool shouldClose = ((MyViewModel) DataContext).TryClose();
if(!shouldClose)
{
e.Cancel = true;
}
}
I'm not an MVVM expert, but in my opinion Yusufs' answer isn't quite MVVM. On the other hand Torpederos answer is a bit complicated for only close cancellation. Here is my approach.
In this example I subscribed to the closing event, but it is always cancelled
private void OnClosing(object sender, CancelEventArgs e)
{
e.Cancel = true;
return;
}
In the XAML I added this
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
<i:Interaction.Triggers>
<i:EventTrigger EventName="Closing">
<i:InvokeCommandAction Command="{Binding Close}" />
</i:EventTrigger>
</i:Interaction.Triggers>
And finally in the view model
public ICommand Close { get; set; }
Close = new RelayCommand(CommandClose);
private void CommandClose(object sender)
{
if (Dirty)
{
// Save your data here
}
Environment.Exit(0);
}
In this approach the the closing event is triggered first. That cancels the closing. After that the interaction trigger is invoked and triggers the code in the view model via the RelayCommand.
In the view model I can use the Dirty flag that is not accessible in the view.
Very good example of doing this in the View Model way can be found in the article of Nish Nishant, where he's using attached properties to hook up window events with commands.
Sample code of attached behaviour (author of the code: Nish Nishant)
public class WindowClosingBehavior {
public static ICommand GetClosed(DependencyObject obj) {
return (ICommand)obj.GetValue(ClosedProperty);
}
public static void SetClosed(DependencyObject obj, ICommand value) {
obj.SetValue(ClosedProperty, value);
}
public static readonly DependencyProperty ClosedProperty
= DependencyProperty.RegisterAttached(
"Closed", typeof(ICommand), typeof(WindowClosingBehavior),
new UIPropertyMetadata(new PropertyChangedCallback(ClosedChanged)));
private static void ClosedChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) {
Window window = target as Window;
if (window != null) {
if (e.NewValue != null) {
window.Closed += Window_Closed;
}
else {
window.Closed -= Window_Closed;
}
}
}
public static ICommand GetClosing(DependencyObject obj) {
return (ICommand)obj.GetValue(ClosingProperty);
}
public static void SetClosing(DependencyObject obj, ICommand value) {
obj.SetValue(ClosingProperty, value);
}
public static readonly DependencyProperty ClosingProperty
= DependencyProperty.RegisterAttached(
"Closing", typeof(ICommand), typeof(WindowClosingBehavior),
new UIPropertyMetadata(new PropertyChangedCallback(ClosingChanged)));
private static void ClosingChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) {
Window window = target as Window;
if (window != null) {
if (e.NewValue != null) {
window.Closing += Window_Closing;
}
else {
window.Closing -= Window_Closing;
}
}
}
public static ICommand GetCancelClosing(DependencyObject obj) {
return (ICommand)obj.GetValue(CancelClosingProperty);
}
public static void SetCancelClosing(DependencyObject obj, ICommand value) {
obj.SetValue(CancelClosingProperty, value);
}
public static readonly DependencyProperty CancelClosingProperty
= DependencyProperty.RegisterAttached(
"CancelClosing", typeof(ICommand), typeof(WindowClosingBehavior));
static void Window_Closed(object sender, EventArgs e) {
ICommand closed = GetClosed(sender as Window);
if (closed != null) {
closed.Execute(null);
}
}
static void Window_Closing(object sender, CancelEventArgs e) {
ICommand closing = GetClosing(sender as Window);
if (closing != null) {
if (closing.CanExecute(null)) {
closing.Execute(null);
}
else {
ICommand cancelClosing = GetCancelClosing(sender as Window);
if (cancelClosing != null) {
cancelClosing.Execute(null);
}
e.Cancel = true;
}
}
}
}
Example how to bind commands:
<Window
x:Class="WindowClosingDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:nsmvvm="clr-namespace:NS.MVVM"
nsmvvm:WindowClosingBehavior.Closed="{Binding ClosedCommand}"
nsmvvm:WindowClosingBehavior.Closing="{Binding ClosingCommand}"
nsmvvm:WindowClosingBehavior.CancelClosing="{Binding CancelClosingCommand}">
Commands "ClosedCommand", "ClosingCommand" and "CancelClosingCommand" should be defined in the separate View-Model.
internal class MainViewModel : ViewModelBase {
private ObservableCollection<string> log = new ObservableCollection<string>();
public ObservableCollection<string> Log {
get { return log; }
}
private DelegateCommand exitCommand;
public ICommand ExitCommand {
get {
if (exitCommand == null) {
exitCommand = new DelegateCommand(Exit);
}
return exitCommand;
}
}
private void Exit() {
Application.Current.Shutdown();
}
private DelegateCommand closedCommand;
public ICommand ClosedCommand {
get {
if (closedCommand == null) {
closedCommand = new DelegateCommand(Closed);
}
return closedCommand;
}
}
private void Closed() {
log.Add("You won't see this of course! Closed command executed");
MessageBox.Show("Closed");
}
private DelegateCommand closingCommand;
public ICommand ClosingCommand {
get {
if (closingCommand == null) {
closingCommand = new DelegateCommand(ExecuteClosing, CanExecuteClosing);
}
return closingCommand;
}
}
private void ExecuteClosing() {
log.Add("Closing command executed");
MessageBox.Show("Closing");
}
private bool CanExecuteClosing() {
log.Add("Closing command execution check");
return MessageBox.Show("OK to close?", "Confirm", MessageBoxButton.YesNo) == MessageBoxResult.Yes;
}
private DelegateCommand cancelClosingCommand;
public ICommand CancelClosingCommand {
get {
if (cancelClosingCommand == null) {
cancelClosingCommand = new DelegateCommand(CancelClosing);
}
return cancelClosingCommand;
}
}
private void CancelClosing() {
log.Add("CancelClosing command executed");
MessageBox.Show("CancelClosing");
}
}
This is another example of canceling the close window directly from ViewModel.
View:
<Window x:Class="WpfApplicationMvvmLight.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Platform"
Title="MainWindow" Height="350" Width="525">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Closing">
<cmd:EventToCommand Command="{Binding Path=ClosingCommand}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
<TextBlock>content...</TextBlock>
</Grid>
ViewModel:
using GalaSoft.MvvmLight.CommandWpf;
using System.ComponentModel;
using System.Windows;
namespace WpfApplicationMvvmLight
{
class SampleViewModel
{
public SampleViewModel() {
_closingCommand = new RelayCommand<CancelEventArgs>(OnClosingCommand);
}
private RelayCommand<CancelEventArgs> _closingCommand;
public RelayCommand<CancelEventArgs> ClosingCommand {
get {
return _closingCommand;
}
}
private void OnClosingCommand(CancelEventArgs e) {
//display your custom message box here..
var result = MessageBox.Show("Do you want to close?", "", MessageBoxButton.YesNoCancel);
//set e.Cancel to true to prevent the window from closing
e.Cancel = result != MessageBoxResult.Yes;
}
}
}
Code behind:
using System.Windows;
namespace WpfApplicationMvvmLight
{
public partial class MainWindow : Window
{
public MainWindow() {
InitializeComponent();
this.DataContext = new SampleViewModel();
}
}
}
This is the reference. MVVM close window event
I have two MediaElements created in my ViewModel and bound in my View. On MediaOne.MediaEnded(), I'm trying to fire another method. BUT, MediaEnded() never fires!
Here is the relevant code:
ViewModel:
private MediaElement _mediaOne;
public MediaElement MediaOne
{
get { return _mediaOne; }
set { _mediaOne = value; NotifyPropertyChanged(); }
}
private MediaElement _mediaTwo;
public MediaElement MediaTwo
{
get { return _mediaTwo; }
set { _mediaTwo = value; NotifyPropertyChanged(); }
}
public SpeechViewModel()
{
_mediaOne = new MediaElement();
_mediaTwo = new MediaElement();
MediaOne.MediaEnded += MediaOne_MediaEnded;
MediaTwo.MediaEnded += MediaTwo_MediaEnded;
}
private async void MediaOne_MediaEnded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
// Do stuff
}
private async void MediaTwo_MediaEnded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
// Do other stuff
}
XAML
<ContentControl Content="{Binding mediaOne}"/>
<ContentControl Content="{Binding mediaTwo}"/>
I have a panel with a button on it that is used to trigger an image capture from an external camera. The capture can take several seconds, so I want the button to disable when capture is in progress. I also want to be able to prevent the user capturing when my program is running a control script. Here is my ViewModel class:
public class CameraControlViewModel : ViewModelBase
{
public CameraControlViewModel()
{
}
public CameraControlViewModel( DataModel dataModel )
: base( dataModel )
{
dataModel.PropertyChanged += DataModelOnPropertyChanged;
_captureImageCommand = new RelayCommand( captureImage );
_capturedImage = new BitmapImage();
_capturedImage.BeginInit();
_capturedImage.UriSource = new Uri( "Images/fingerprint.jpg", UriKind.Relative );
_capturedImage.CacheOption = BitmapCacheOption.OnLoad;
_capturedImage.EndInit();
}
public ICommand CaptureImageCommand
{
get { return _captureImageCommand; }
}
public bool CanCaptureImage
{
get { return !dataModel.IsScriptRunning && !_captureInProgress; }
}
public bool IsCaptureInProgress
{
get { return _captureInProgress; }
set
{
if (_captureInProgress != value)
{
_captureInProgress = value;
OnPropertyChanged( "IsCaptureInProgress" );
OnPropertyChanged( "CanCaptureImage" );
}
}
}
public int PercentDone
{
get { return _percentDone; }
set
{
if (_percentDone != value)
{
_percentDone = value;
OnPropertyChanged( "PercentDone" );
}
}
}
public BitmapImage CapturedImage
{
get { return _capturedImage; }
}
private void DataModelOnPropertyChanged( object sender, PropertyChangedEventArgs propertyChangedEventArgs )
{
string property = propertyChangedEventArgs.PropertyName;
if (property == "IsScriptRunning")
{
OnPropertyChanged( "CanCaptureImage" );
}
OnPropertyChanged( property );
}
private void captureImage( object arg )
{
IsCaptureInProgress = true;
PercentDone = 0;
// TODO: remove this placeholder.
new FakeImageCapture( this );
// TODO (!)
}
internal void captureComplete()
{
IsCaptureInProgress = false;
}
// Remove this placeholder when we can take images.
private class FakeImageCapture
{
CameraControlViewModel _viewModel;
int _count;
Timer _timer = new Timer();
public FakeImageCapture( CameraControlViewModel viewModel )
{
this._viewModel = viewModel;
_timer.Interval = 50;
_timer.Elapsed += TimerOnTick;
_timer.Start();
}
private void TimerOnTick( object sender, EventArgs eventArgs )
{
++_count;
if (_count <= 100)
{
_viewModel.PercentDone = _count;
}
else
{
Application.Current.Dispatcher.Invoke( (Action)_viewModel.captureComplete );
_timer.Stop();
_timer = null;
_viewModel = null;
}
}
}
private readonly ICommand _captureImageCommand;
private volatile bool _captureInProgress;
private BitmapImage _capturedImage;
private int _percentDone;
}
Here is the XAML for the button:
<Button Command="{Binding CaptureImageCommand}"
Grid.Row="0" Grid.Column="0"
Margin="4"
IsEnabled="{Binding CanCaptureImage}"
ToolTip="Capture Image">
<Image Source="../Images/camera-icon.gif" Width="64" Height="64" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Button>
Clicking the "capture" button goes fine. The button disables and elsewhere a progress bar appears showing the (currently faked) image capture progress. However, when the capture completes, even though I set the CanCaptureImage property in the captureComplete() method, the button does not change back to its "enabled" appearance. It will only do this when I click somewhere (anywhere) in the window. However, the button is actually enabled because I can click on it again to trigger a 2nd capture.
I have tried CommandManager.InvalidateRequerySuggested() inside captureComplete() but that doesn't help. Any ideas?
Rather than having a separate IsEnabled binding to enable/disable the button, you should really just use the CanExecute predicate of the RelayCommand: http://msdn.microsoft.com/en-us/library/hh727783.aspx
This would ensure that the button will get enabled/disabled properly when calling CommandManager.InvalidateRequerySuggested(). Get rid of the CanCaptureImage property and modify your code as follows:
public CameraControlViewModel( DataModel dataModel )
: base( dataModel )
{
dataModel.PropertyChanged += DataModelOnPropertyChanged;
_captureImageCommand = new RelayCommand( captureImage, captureImage_CanExecute );
_capturedImage = new BitmapImage();
_capturedImage.BeginInit();
_capturedImage.UriSource = new Uri( "Images/fingerprint.jpg", UriKind.Relative );
_capturedImage.CacheOption = BitmapCacheOption.OnLoad;
_capturedImage.EndInit();
}
private bool captureImage_CanExecute( object arg)
{
return !dataModel.IsScriptRunning && !_captureInProgress;
}