Update bound property immediately from UI thread - wpf

Most questions I see about this are asking the opposite - how to update the UI from a non-UI-thread, or a background thread.
In my case, I want the opposite. Is there anything I can call or do in the command running on the UI thread to say Update the UI now? I think this would be something like DoEvents used to be. Trying to keep the question a bit generic so it can hopefully help someone else sometime.
Things I've tried...
I've simplified my command function, just to see if I can get the ProgressText updates to work. None of this works. After the entire function executes, only the last text shows up, which is Progress Update 5. I wanted to see it go through each update text in turn.
Private Sub MoveOrRenameSongs()
System.Threading.Thread.Sleep(2000)
ProgressText = "Progress update 1"
Application.Current.MainWindow.UpdateLayout()
System.Threading.Thread.Sleep(2000)
ProgressText = "Progress update 2"
Application.Current.MainWindow.UpdateLayout()
System.Threading.Thread.Sleep(2000)
ProgressText = "Progress update 3"
Application.Current.MainWindow.UpdateLayout()
System.Threading.Thread.Sleep(2000)
ProgressText = "Progress update 4"
Application.Current.MainWindow.UpdateLayout()
System.Threading.Thread.Sleep(2000)
ProgressText = "Progress update 5"
Application.Current.MainWindow.UpdateLayout()
Dim myWindow As MainWindow = Application.Current.MainWindow
System.Threading.Thread.Sleep(2000)
ProgressText = "Progress update 1"
myWindow.UpdateLayout()
System.Threading.Thread.Sleep(2000)
ProgressText = "Progress update 2"
myWindow.UpdateLayout()
System.Threading.Thread.Sleep(2000)
ProgressText = "Progress update 3"
myWindow.UpdateLayout()
System.Threading.Thread.Sleep(2000)
ProgressText = "Progress update 4"
myWindow.UpdateLayout()
System.Threading.Thread.Sleep(2000)
ProgressText = "Progress update 5"
myWindow.UpdateLayout()
End Sub
More Background and details...
I have a Command that can run for a bit longer, and I'd like to update some progress text as it goes along. However, because the command function is running in the UI thread, it's blocking the UI, and the progress text in the UI cannot update. I get that.
ProgressText is bound to a dependency property, that works fine in other command functions that run instantly.
<StatusBar DockPanel.Dock="Bottom" Height="26">
<StatusBarItem HorizontalAlignment="Left" >
<TextBlock Text="{Binding StatusText}" Width="Auto"></TextBlock>
</StatusBarItem>
<StatusBarItem HorizontalContentAlignment="Stretch">
<TextBlock Text="{Binding ProgressText}"></TextBlock>
</StatusBarItem>
</StatusBar>
Private _progressText As String
Public Property ProgressText() As String
Get
Return _progressText
End Get
Set(ByVal value As String)
_progressText = value
RaisePropertyChanged("ProgressText")
End Set
End Property
I have tried putting the processing to a background thread, but then I run into other issues. I will list a few here, but they are beside the point.
I can call Dispatcher.Invoke to update ProgressText, BUT
I can no longer open a window that I need to collect some input; thread needs to be STA
I figured out how to make the thread STA, then I can't access the parent to set the position of the input window
get past all that, and my bound list no longer updates in the UI when I update it at the end, because the code is all written for being on the UI thread, so those updates don't work
As a result I would like to explore staying on the UI thread. This is a personal application that I'm writing as a hobby. I don't care if the UI is locked for a bit, but would like to see the progress as it does the work.
What worked in the end
Each of the 5 progress texts appears in succession as expected.
Private Async Function MoveOrRenameSongs() As System.Threading.Tasks.Task
System.Threading.Thread.Sleep(1000)
ProgressText = "Progress update 1"
Await System.Threading.Tasks.Task.Delay(100)
System.Threading.Thread.Sleep(1000)
ProgressText = "Progress update 2"
Await System.Threading.Tasks.Task.Delay(100)
System.Threading.Thread.Sleep(1000)
ProgressText = "Progress update 3"
Await System.Threading.Tasks.Task.Delay(100)
System.Threading.Thread.Sleep(1000)
ProgressText = "Progress update 4"
Await System.Threading.Tasks.Task.Delay(100)
System.Threading.Thread.Sleep(1000)
ProgressText = "Progress update 5"
Await System.Threading.Tasks.Task.Delay(100)
End Function

You can just free up the ui thread briefly and give it a bit of time to do things.
Make your command async. Exactly how easy that is depends on how you do you icommands.
With the community mvvm toolkit you can create a relaycommand with an async task:
[RelayCommand]
private async Task SaveTransaction()
{
// Expensive code
await Task.Delay(100);
// Expensive code
}
Awaiting that task.delay compiles into a timer and your code is split so it resumes after 100 ms. It'll pause briefly BUT not block the UI thread like a thread.sleep would. Whatever has pile up in the dispatcher queue will start processing. Then the code resumes.
If you have no such async friendly command implementation you could consider:
https://johnthiriet.com/mvvm-going-async-with-async-command/

Related

UI not updating PropertyChanged with MVVM

Is it ever possible that UI skips updating itself although the Visibility of the UI component is binded to ViewModel property and PropertyChanged for that property is implemented?
View/XAML:
<Border Visibility="{Binding ShowLoadingPanel, Converter={StaticResource BoolToHiddenConverter}}">
<TextBlock Text="LOADING..." />
</Border>
ViewModel:
Public Property ShowLoadingPanel As Boolean
Get
Return _showLoadingPanel
End Get
Set(value As Boolean)
_showLoadingPanel = value
OnPropertyChanged("ShowLoadingPanel")
End Set
End Property
When running the following from ViewModel:
ShowLoadingPanel = True
RunBigTask() 'runs a task that takes a long time
ShowLoadingPanel = False
...the Border defined in XAML doesn't become visible.
But if I add something requiring user interaction, for example like:
ShowLoadingPanel = True
MsgBox("Click to continue")
RunBigTask() 'runs a task that takes a long time
ShowLoadingPanel = False
... then the border becomes visible as desired.
How is that possible?
You should really run your long running task in a background thread because it is blocking your UI thread from updating the Visibility... as it is, the Visibility should update when the long running task is complete.
It is quite common for users to use a BackgroundWorker object to do this. You can find a complete working example on the BackgroundWorker Class page on MSDN.
A common alternative to the BackgroundWorker would be to use a Task object to run your long running process asynchronously. You can find a full working example of using a Task on the Task Class page on MSDN.
You are blocking the Dispatcher, preventing the layout from being updated. When you open a Message Box, you push a nested message loop that allows the Dispatcher to continue processing its queue until the Message Box is closed. The layout updates are happening during that period.
The same thing happens when you call ShowDialog() on a regular Window: your code blocks, but the Dispatcher keeps running so the UI updates as expected. Your code does not resume until the nested message loop is popped, which happens automatically when you close a modal dialog (like your Message Box).
I'm using C#, and in our case Visiblity is not a boolean, but an enum: System.Windows.Visibility with values of Hidden / Visible/ Collapsed.
The same seems true for VB : Public Property Visibility As Visibility

WPF: Show and hide user controls after user interaction

I have got the following window which contains my user controls in the upper area (depending on the state of the process) and a button. After click at the button one user control will be hidden and another will be shown. By the change from the first to the second control the window is frozen until the second control is finished with the tasks.
The second control contains a ListView which logs the steps. I want to see this logging directly during the process. But I can only see it when all is finish. How is it possible to refresh the window before the second control starts with it work?
A code line like
Me.UpdateLayout()
doesn't work ...
The whole program shalls be an update tool with three screens (1. settings, 2. logging of the update process, 3. finish dialog). Therefore I need to know the single steps of the logging directly after done.
My current XAML is the following:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:SiS.Controls="clr-namespace:SiSConverter"
Title="Konvertierung von SiS-Anwendungen"
Height="400" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="50px" />
</Grid.RowDefinitions>
<SiS.Controls:Settings x:Name="ucSettings" />
<SiS.Controls:Upgrade x:Name="ucUpgrade" />
<SiS.Controls:Finish x:Name="ucFinish" />
<Button Name="btnContinue" Width="100px" Height="30px" Grid.Row="1" Margin="0,0,10,0" HorizontalAlignment="Right" />
</Grid>
</Window>
And the corresponding code behind:
Class MainWindow
Private _Step As Integer = -1
Private Property [Step] As Integer
Get
Return _Step
End Get
Set(value As Integer)
_Step = value
Me.ucSettings.Visibility = Visibility.Collapsed
Me.ucUpgrade.Visibility = Visibility.Collapsed
Me.ucFinish.Visibility = Visibility.Collapsed
Me.btnContinue.Content = "Weiter"
Select Case _Step
Case 0
Me.ucSettings.Visibility = Visibility.Visible
Case 1
Me.ucUpgrade.Visibility = Visibility.Visible
Case 2
Me.ucFinish.Visibility = Visibility.Visible
Me.btnContinue.Content = "Beenden"
Case Else
End Select
Me.UpdateLayout() 'doesn't work
End Set
End Property
Private Sub MainWindow_Initialized(sender As Object, e As EventArgs) Handles Me.Initialized
Me.Step = 0
For Each Item As System.IO.FileInfo In New System.IO.DirectoryInfo("Converters").GetFiles()
Dim oConverter As ISiSConverter = System.Reflection.Assembly.LoadFrom(Item.FullName).CreateInstance("Upgrade.Main", True)
Me.ucSettings.Converters.Add(oConverter)
Next
End Sub
Private Sub btnContinue_Click(sender As Object, e As RoutedEventArgs) Handles btnContinue.Click
Select Case Me.Step
Case 0 'Einstellungen
Me.Step += 1
Me.btnContinue.IsEnabled = False
Me.ucSettings.Converters.FindAll(Function(item) item.DoUpgrade).ForEach(Sub(item) item.Upgrade())
Me.btnContinue.IsEnabled = True
Case 1 'Upgrade
Me.Step += 1
Case 2 'Abschluss
Me.Close()
Case Else
End Select
End Sub
End Class
Thanks for any response.
Edit:
The program consists of three steps. In the first step will be configured what is to do.
With click on the bottom right button shall be done something for each marked item (green button, red will be ignored) which is shown by the list of the second step.
If step 2 is done the finish Screen is shown and the program can be closed (this screen is still to design).
In WinForms and ASP.NET I show and hide user controls also in the way I did this time (maybe also this wasn't before a good implementation). It would be nice if there is in WPF a better possibility.
Your problem here is your UI thread which is the only one that can update the elements shown by your View / Window is too busy running your
Me.ucSettings.Converters.FindAll(Function(item) item.DoUpgrade).ForEach(Sub(item) item.Upgrade())
^^ function and hence cannot update the view / render elements until it's done.
A "cheat" kind of to explicitly show this (Just use this as information, don't use it in your code)
Application.Current.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background,
Sub()
Me.ucSettings.Converters.FindAll(Function(item) item.DoUpgrade).ForEach(Sub(item) item.Upgrade())
End Sub)
^^ That will show your new UserControl when the button is clicked. You still however have the issue of un-responsive UI.
You need to look at offloading work from the main thread by using things like BackgroundWorkers for what your doing from code-behind to keep the UI responsive and then whenever you need to update a UI control Notify the UI thread accordingly.

Mvvm - Cancel changes in Wpf Listbox, vb.net

I have a wpv/mvvm-light/vb.net application with a master/detail view. In this view there is a listbox of clients and a detail view of the client's details where they user can view and edit the customers.
I wanted to add a function where users would be prompted to save changes when a new client is selected in the listbox. If the user chooses yes from the messagebox then save changes and if no then discard changes and return previous selected item back to its original value. I have this all working fine.
My problem is that when the user selects a new client and the messagebox asks them to save changes, the listbox goes out of sync. Meaning that the listbox shows the new client selected but the detail view still shows the previous client. The odd thing is that it works properly on rare occasions.
The following is my view:
<UserControl x:Class="FTC.View.ClientListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FTC_Application"
mc:Ignorable="d"
d:DesignHeight="400" d:DesignWidth="900">
<ListBox
Grid.Column="1"
Width="350"
Style="{DynamicResource FTC_ListBox}"
ItemTemplate="{DynamicResource FTC_ClientListTemplate}"
ItemContainerStyle="{DynamicResource FTC_ListItem}"
ItemsSource="{Binding ClientViewSource.View}"
SelectedItem="{Binding Path=Selection, Mode=TwoWay}"
/>
<ContentControl DataContext="{Binding Path=Selection, Mode=TwoWay}" >
<!--all the display stuff goes here for the detail view-->
</ContentControl>
</UserControl>
the following is the property in the viewmodel that the selecteditem of the listbox is bound to. It is also the binding for the content control that displays the details.
Public Property Selection As client
Get
Return Me._Selection
End Get
Set(ByVal value As client)
''capture current value of selection
_PreviousClient = _Selection
''If they are the same,
If value Is _PreviousClient Then
Return
End If
' Note that we actually change the value for now.This is necessary because WPF seems to query the
' value after the change. The list box likes to know that the value did change.
If Me._Selection.HasChanges = True And _Selection.HasErrors = False Then
'If HasChangesPrompt(value) = True Then
' ''user rejects saving changes, exit property
' Return
'End If
If FTCMessageBox.Show("Do you want to save your changes", "Unsaved Changes", MessageBoxButton.YesNo, MessageBoxImage.Warning) = MessageBoxResult.No Then
''SELECTION IS CANCELLED
' change the value back, but do so after the UI has finished it's current context operation.
Application.Current.Dispatcher.BeginInvoke(New Action(Sub()
'' revert the current selected item to its original values and reset its HasCHanges tracking
objHelper.CopyProperties(_OriginalClient, _Selection)
_Selection.HasChanges = False
RaisePropertyChanged(ClientSelectedPropertyName)
''continue with listbox selection changing to the new value for selection
_ClientCollectionViewSource.View.MoveCurrentTo(value)
End Sub), DispatcherPriority.Normal, Nothing)
Return
Else
''save changes to database
SaveExecute()
End If
End If
_Selection = value
_Selection.HasChanges = False
RaisePropertyChanged(ClientSelectedPropertyName)
''clone the unchanged version of the current selected client on na original variable
objHelper.CopyProperties(_Selection, _OriginalClient)
End Set
End Property
SO the idea is that if the user does not want to save changes, an original value of the client is copied (using reflection) over the current value, then the ui is updated and the selection continues on to the new value chosen by the user. However, like I said above, the listbox does not reflect this change even though I tired to hard code it with the following line:
''continue with listbox selection changing to the new value for selection
_ClientCollectionViewSource.View.MoveCurrentTo(value)
I got this solution by working customizing the solution posted HERE
can anyone help me figure out why my listbox goes out of sync when this happens.
Thanks in advance
First:
I can't find the real Problem in your solution, but you have definitly - and I repeat - definitly too much code and logic in your Property Setter. Try move it to other methods and validate your implementation of those many ´if else´ blocks.
Second:
The Setter gets only fired when you select a new Item in your Listbox, but you Raise a Property changes for ´ClientSelectedPropertyName´ and not for ´Selection´ as its supposed to be. Move the property changed alsways to the end of your setter.
Try this. I hope it helps :)
So I have a working example that I think follows the MVVM-Light standard. There is a lot going on so I will try to keep it short and precise.
I ended up using EventToCommand bound to the SelectionChanged event with a ListView(instead of listbox). The EventToCommand required new namespace references as is shown below. I then bound the EventToCommand to a RelayCommand in the view model which in turn calls a private sub that handles the client validation and saves/cancels/ and updates the listview seleceditem as required.
For further information, I have a navigation service that is used to navigat between views in my wpf application. I used the MVVM-Light messanger to send a navigationstarting message that is "recieved" by this view model. Then the same client validation functions are performed and naviagtion is canceled/allowed based on user response to the dialog message thrown. I will no include all of hte navigation code unless requested. The following is the code needed to solve my original question.
<UserControl x:Class="FTC.View.ClientListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FTC_Application"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="http://www.galasoft.ch/mvvmlight"
mc:Ignorable="d"
d:DesignHeight="400" d:DesignWidth="900">
<ListView
Grid.Column="1"
Width="350"
Style="{DynamicResource FTC_ListView}"
ItemTemplate="{DynamicResource FTC_ClientListTemplate}"
ItemContainerStyle="{DynamicResource FTC_ListViewItem}"
ItemsSource="{Binding ClientViewSource.View}"
SelectedItem="{Binding Path=Selection, Mode=TwoWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<cmd:EventToCommand Command="{Binding SelectedItemChangedCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListView>
<ContentControl DataContext="{Binding Path=Selection, Mode=TwoWay}" >
<!-- Display stuff and bound controls go here -->
</ContentControl>
</Grid>
</UserControl>
Then the following is the relevant code (I removed as much code as possible to keep it clear) in my view model:
Imports System.Data
Imports System.ComponentModel
Imports System.Collections.ObjectModel
Imports System.Windows.Threading
Imports GalaSoft.MvvmLight
Imports GalaSoft.MvvmLight.Command
Imports GalaSoft.MvvmLight.Messaging
Imports FTCModel
Imports FTC_Application.FTC.Model
Imports FTC_Application.FTC.View
Imports FTC_Application.FTC.ViewModel
Imports FTC_Application.FTC.MessageBox
Imports FTC_Application.FTC.Helpers
Imports FTC_Application.FTC.MessengerHelper
Namespace FTC.ViewModel
Public Class ClientListViewModel
Inherits ViewModelBase
Implements IDataErrorInfo
#Region "DECLARATIONS"
Public Const ClientCollectionPropertyName As String = "ClientCollection"
Public Const ClientSelectedPropertyName As String = "Selection"
Public Const ClientDetailCollectionPropertyName As String = "ClientDetailCollection"
Public Const ClientPropertyName As String = "Client"
''gets the data from LINQ to ENT Model
Private _Clients As New ObservableCollection(Of client)
''creats holder for the selected item two way binding
Private _Selection As New client
''the following is used to track changes for unding and canceling selection changed
Private _PreviousClient As New client
Private _PreviousOriginalClient As New client
Private _OriginalClient As New client
''Recieves observable collection and provicdes sorting and filtering function
Private _ClientCollectionViewSource As New CollectionViewSource
''RELAY COMMANDS declarations
Private _SaveCommand As RelayCommand
Private _SelectedItemChangedCommand As RelayCommand
''gets the VML for getting the data service
Private vml As ViewModelLocator = TryCast(Application.Current.Resources("Locator"), ViewModelLocator)
''this is a holder for the client data service
Private _ClientAccess As IClientDataService = vml.Client_Service
'' has functions using reflection for copying objects
Dim objHelper As New ObjectHelper
''tracks if client validation is coming from navigation or listview selecteditemchanged
Private bNavigatingFlag As Boolean = False
#End Region
#Region "PROPERTIES"
Public ReadOnly Property ClientViewSource As CollectionViewSource
Get
Return Me._ClientCollectionViewSource
End Get
End Property
Private Property Clients As ObservableCollection(Of client)
Get
Return Me._Clients
End Get
Set(ByVal value As ObservableCollection(Of client))
Me._Clients = value
_Clients = value
RaisePropertyChanged(ClientCollectionPropertyName)
End Set
End Property
Public Property Selection As client
Get
Return Me._Selection
End Get
Set(ByVal value As client)
''capture current value of selection
_PreviousClient = _Selection
objHelper.CopyProperties(_OriginalClient, _PreviousOriginalClient)
''If they are the same,
If value Is _PreviousClient Then
Return
End If
_Selection = value
_Selection.HasChanges = False
RaisePropertyChanged(ClientSelectedPropertyName)
''clone the unchanged version of the current selected client on na original variable
objHelper.CopyProperties(_Selection, _OriginalClient)
End Set
End Property
#End Region
#Region "COMMANDS"
Public ReadOnly Property SelectedItemChangedCommand() As RelayCommand
Get
If _SelectedItemChangedCommand Is Nothing Then
_SelectedItemChangedCommand = New RelayCommand(AddressOf SelectionChangedValidate)
End If
Return _SelectedItemChangedCommand
End Get
End Property
#End Region
#Region "METHODS"
Private Sub SelectionChangedValidate()
''Uses falg to tell if validation request triggered by navigation event or listview selecteditemchanged event
''use previous client for listview event and current client for navigating event
Dim _ClientToValidate As client
If bNavigatingFlag = True Then
_ClientToValidate = _Selection
Else
_ClientToValidate = _PreviousClient
End If
If _ClientToValidate.HasChanges = True And _ClientToValidate.HasErrors = False Then
Dim message = New DialogMessage(_ClientToValidate.chrCompany.ToString + " has been changed." + vbCrLf + "Do you want to save your changes?", AddressOf SavePreviousResponse) With { _
.Button = MessageBoxButton.YesNo, _
.Caption = "Unsaved Changes" _
}
Messenger.[Default].Send(message)
Exit Sub
End If
If _ClientToValidate.HasErrors = True Then
Dim message = New DialogMessage(_ClientToValidate.chrCompany.ToString + " has errors." + vbCrLf + "You must correct these errors before you can continue.", AddressOf HasErrorsResponse) With { _
.Button = MessageBoxButton.OK, _
.Caption = "Validation Error" _
}
Messenger.[Default].Send(message)
Exit Sub
End If
''reset the navigation flag
bNavigatingFlag = False
End Sub
Private Sub SavePreviousResponse(result As MessageBoxResult)
If result = MessageBoxResult.No Then
objHelper.CopyProperties(_PreviousOriginalClient, _PreviousClient)
_PreviousClient.HasChanges = False
Else
''user wants to save changes, save changes to database
SaveExecute()
End If
End Sub
Private Sub HasErrorsResponse(result As MessageBoxResult)
Selection = _PreviousClient
''_ClientCollectionViewSource.View.MoveCurrentTo(_PreviousClient)
End Sub
Private Function HasChangesPrompt(value As client) As Boolean
If FTCMessageBox.Show("Do you want to save your changes", "Unsaved Changes", MessageBoxButton.YesNo, MessageBoxImage.Warning) = MessageBoxResult.No Then
'' change the selected client back to its original value, but do so after the UI has finished its current context operation.
Application.Current.Dispatcher.BeginInvoke(New Action(Sub()
'' revert the current selected item to its original values and reset its HasCHanges tracking
objHelper.CopyProperties(_OriginalClient, _Selection)
_Selection.HasChanges = False
RaisePropertyChanged(ClientSelectedPropertyName)
''continue with listbox selection changing to the new value for selection
_ClientCollectionViewSource.View.MoveCurrentTo(value)
End Sub), DispatcherPriority.Normal, Nothing)
Return True
Else
''user wants to save changes, save changes to database
Return False
SaveExecute()
End If
End Function
#End Region
Public Sub New()
Clients = _ClientAccess.GetClient_All
''Sets the observable collection as the source of the CollectionViewSource
_ClientCollectionViewSource.Source = Clients
If Selection.idClient = 0 Then
Selection = Clients.Item(0)
End If
''register for messages
Messenger.[Default].Register(Of String)(Me, AddressOf HandleMessage)
End Sub
End Class
End Namespace
INXS, you will notice that the selection property setter has way less code/logic. Also, I think each part of the view model is testable and there is no direct coupling between my view and viewmodel. But this is my first WPF/MVVM application so I sitll don't fully grasp all the concepts.
I hope that this can help someone as it took my quite a while to figure it out.

WPF & RelayCommand - Button always firing

I have been working through some of the examples of MVVM & WPF and while doing some debugging I find that the RelayCommand associated with a button on my view is constantly firing (executing the associated ImportHoursCommand) as soon as the program starts.
Here are the code snippets:
View
<Button x:Name="ImportHoursButton" Content="Import Hours"
Command="{Binding ImportHoursCommand}"
Height="25" Width="100" Margin="10"
VerticalAlignment="Bottom" HorizontalAlignment="Right"
Grid.Row="1" />
ViewModel
private RelayCommand _importHoursCommand;
public ICommand ImportHoursCommand
{
get
{
if (_importHoursCommand == null)
{
_importHoursCommand = new RelayCommand(param => this.ImportHoursCommandExecute(),
param => this.ImportHoursCommandCanExecute);
}
return _importHoursCommand;
}
}
void ImportHoursCommandExecute()
{
MessageBox.Show("Import Hours",
"Hours have been imported!",
MessageBoxButton.OK);
}
bool ImportHoursCommandCanExecute
{
get
{
string userProfile = System.Environment.GetEnvironmentVariable("USERPROFILE");
string currentFile = #userProfile + "\\download\\test.txt";
if (!File.Exists(currentFile))
{
MessageBox.Show("File Not Found",
"The file " + currentFile + " was not found!",
MessageBoxButton.OK);
return false;
}
return true;
}
}
If I put a breakpoint on the 'string userProfile = ...' line and run the program, Visual Studio will stop on the breakpoint and continue to stop on the breakpoint everytime I click the debug 'Continue' button. If I don't have a breakpoint the program appears to run OK but should this command always be checking if it can execute?
I am using the RelayCommand from Josh Smith's article here.
If a Button is bound to a Command, the CanExecute() determines if the Button is enabled or not. This means the CanExecute() is run anytime the button needs to check it's enabled value, such as when it gets drawn on the screen.
Since you're using a breakpoint in VS, I am guessing that the application is getting hidden when VS gains focus, and it is re-drawing the button when you hit the Continue button. When it re-draws the button, it is evaluating CanExecute() again, which goes into the endless cycle you're seeing
One way to know for sure is to change your breakpoint to a Debug.WriteLine, and watch your output window when your application is running.
As a side note, you could also change your RelayCommand to Microsoft Prism's DelegateCommand. I haven't looked at the differences too closely, however I know RelayCommands automatically raise the CanExecuteChanged() event when certain conditions are met (properties change, visual invalidated, etc) while DelegateCommands will only raise this event when you specifically tell it to. This means CanExecute() only evaluates when you specifically tell it to, not automatically, which can be good or bad depending on your situation.
That's perfectly normal; WPF reevaluates whether the command can be executed very often, for instance when the focus control changes, or when the window takes focus. Every time you click "Continue", the window takes the focus again, which reevaluates the CanExecute of your command, so your breakpoint is hit again.

How to force binding evaluation every time a ContextMenu is opened?

I have a ContextMenu with a MenuItem in it:
<Grid>
<Button Content="{Binding Test}">
<Button.ContextMenu>
<ContextMenu>
<StackPanel>
<MenuItem Header="{Binding Test}"/>
</StackPanel>
</ContextMenu>
</Button.ContextMenu>
</Button>
</Grid>
The Test property looks like the following:
private Random rand;
public string Test
{
get
{
return "Test " + this.rand.Next(50);
}
}
When I right click the button, I have, for instance "Test 41". Next times I open the menu I have the same value. Is there a way to force the Menu to evaluate the binding each time ? (and then having "Test 3", "Test 45", "Test 65"...
Here is a hack i use in the same situation:
Name your context menu and create your own RoutedCommand, i use these for all buttons and menus as they have a CanExecute method which enables or disables the control and an Execute method that gets called to do the work. every time a context menu opens the CanExecute method gets called. that means you can do custom processing to see if it should be enabled, or you can change the contents of the menu, good for changing menu's when saving different things. we use it to say, Save xyx.. when the user is editing an xyx.
anyway if the menu is named you can modify its content on the CanExecute. (if the command originates on the menu you will have it as the sender of the event CanExecute anyway, but sometimes i like to scope them higher as you can assign keyboard shortcuts to them which can be executed from anywhere they are scoped.)
Your Test property needs to inform other components whenever its value changes, e.g. by implementing the INotifyPropertyChanged interface in the containing class like this:
class Window1 : Window, INotifyPropertyChanged {
...
private string m_Test;
public string Test {
get {
return m_Test;
}
set {
m_Test = value;
OnPropertyChanged("Test");
}
}
}
You can then modify the value of Test from anywhere by using the property (Test = "newValue";) and the changes will be reflected on the UI.
If you really need to change the value of the property when the ContextMenu is shown, use the Opend event of the ContextMenu:
Xaml:
<ContextMenu Opened="UpdateTest">
<MenuItem Header="{Binding Test}" />
</ContextMenu>
Code-behind:
private void UpdateTest(object sender, RoutedEventArgs e) {
// just assign a new value to the property,
// UI will be notified automatically
Test = "Test " + this.rand.Next(50);
}

Resources