Async progress bar in WPF using binding - wpf

I have a problem with visualizing the MVVM WPF progress bar. My methods for the main script runs but the progress bar isn't updating at all. I could use tips on getting in the right direction. In my previous codes, I was given tips to use Progress<T> (by #Jonathan Willcock) but I couldn't implement it successfully.
(Please note that this post is not a repeated question because last time, I used button click but I want it to purely be run on data binding)
Question
How do I asynchronously bind the progress bar to the method which is called in my view models? I am using Delegate command to call my methods and I do not want to use the button click event.
What I have in the view - XAML
I have two main functions here, the progress bar and the button. The button starts the method calling successfully but the progress bar doesn't load.
<ProgressBar Name="pbStatus"
Minimum="0"
Value="{Binding PrgBarVal, Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"
Maximum="100"
Height="20"/>
<Button x:Name = "ApplyButton" Margin="0 1 0 1" Content ="Run software" Command="{Binding RunCalcBtn, Mode=TwoWay}"/>
What I have in the XAML.cs
What I know from the background worker method is that it tries to run it async. I have an issue here, I added a breakpoint here to check if the code runs through the 'Window_ContentRendered' method but it doesn't.
public partial class UserInterface: UserControl
{
public UserInterface()
{
InitializeComponent();
}
private void Window_ContentRendered(object sender, EventArgs e)
{
BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.DoWork += worker_DoWork; //Do I need a worker.ProgressChanged here?
worker.RunWorkerAsync();
}
thenamespace.ViewModel.SelectedViewModel get = new thenamespace.ViewModel.SelectedViewModel ();
void worker_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = Convert.ToInt32(get.PrgBarVal); i < 100; i++)
{
(sender as BackgroundWorker).ReportProgress(i);
}
}
}
What I have in the View Model
In the view model, I am using delegate command to call my data binding methods.
The public SelectedViewModel() is where the Run() method is called. In the Run() method, the functions in there runs correctly and updates the value of PrgBarVal correctly. However, this doesn't update the progress bar at all.
public class SelectedViewModel : ModelView, INotifyPropertyChanged
{
public ModelView modelView { get; set; }
private DelegateCommand _runCalcBtn;
public DelegateCommand RunCalcBtn
{
get { return _runCalcBtn; }
set
{
_runCalcBtn = value;
SetPropertyChanged("RunCalcBtn"); //Same as RaisedPropertyChanged
}
}
public SelectedViewModel()
{
modelView = new ModelView();
RunCalcBtn = new DelegateCommand(Run);
}
private int _prgBarVal;
public int PrgBarVal
{
get { return _prgBarVal; }
set
{
_prgBarVal = value;
OnPropertyChanged("PrgBarVal"); //Same as RaisedPropertyChanged
}
}
//Main Function
private async void Run()
{
MethodToWork(); //Function that calls other methods to run
}
Thank you very much for your help!

Here is the ViewModel (used NuGet ReactiveUI.WPF)
public class MainViewModel : ReactiveObject
{
private int _workProgress;
public MainViewModel()
{
IProgress<int> progress = new Progress<int>( e => WorkProgress = e );
StartWork = ReactiveCommand.CreateFromTask( () => ExecuteStartWorkAsync( progress ) );
}
private async Task ExecuteStartWorkAsync( IProgress<int> progress )
{
progress.Report( 0 );
for ( int i = 0; i < 1000; i++ )
{
await Task.Delay( 10 ).ConfigureAwait( false );
progress.Report( (int)Math.Floor( i / 1000.0 * 100.0 ) );
}
}
public int WorkProgress { get => _workProgress; private set => this.RaiseAndSetIfChanged( ref _workProgress, value ); }
public ICommand StartWork { get; }
}
and the View
<Window x:Class="WpfApp4.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp4"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<Button Content="Start work" Command="{Binding StartWork}" Width="80"/>
<ProgressBar Value="{Binding WorkProgress,Mode=OneWay}" Width="120"/>
</StackPanel>
</Grid>
</Window>

Related

WPF ProgressBar not updating

This is casual and prototype code, hence me trying what I think should work, googling around if it doesn't, then asking here after perusing similar questions.
I have the following markup in my Shell view:
<StatusBarItem Grid.Column="0">
<TextBlock Text="{Binding StatusMessage}" />
</StatusBarItem>
<Separator Grid.Column="1" />
<StatusBarItem Grid.Column="2">
<ProgressBar Value="{Binding StatusProgress}" Minimum="0" Maximum="100" Height="16" Width="198" />
</StatusBarItem>
Then in ShellViewModel I have the following two properties and an event handler:
private string _statusMessage;
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
private double _statusProgress;
public double StatusProgress
{
get => _statusProgress;
set => SetProperty(ref _statusProgress, value);
}
private void OnFileTransferStatusChanged(object sender, FileTransferStatusEventArgs fileTransferStatusEventArgs)
{
StatusMessage = fileTransferStatusEventArgs.RelativePath;
StatusProgress = fileTransferStatusEventArgs.Progress;
}
The event is raised periodically, i.e. every n iterations, from a file download helper class.
Now the strange thing is this, when the event handler updates the vm properties, on the Shell view, the TextBlock bound to StatusMessage updates and displays correctly, but the ProgressBar bound to StatusProgress does not, and remains blank. If I put a break-point in the event handler, I can see the StatusProgress property being properly updated in various values from 0 to 100, yet this does not reflect on the ProgressBar.
The idea of the event handler executing on another thread, which often causes UI update problems, occurred to me, but why is one UI element updating properly and the other not?
NOTE: I have been monumentally stupid and not tested the ProgressBar statically, i.e. just set the viewmodel's StatusProgress to a value and get the shell window to display, without going through the download loop. If I do this, the progress bar displays a length that more or less corresponds to its Value property. None of the layout change suggestions made in comments or answers changes this. Statically it is always visible and always displays a value.
EXAMPLE: I created a small example that believe duplicates the problem. In the example the progress bar doesn't update until the waited on task has completed, and I believe this is the case with my main question, but it was a long download, and I didn't wait for it to complete before noticing the progress bar wasn't updating.
Here is the StatusBar in `MainWindow.xaml:
<StatusBar DockPanel.Dock="Bottom" Height="20">
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2" />
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<StatusBarItem Grid.Column="2">
<ProgressBar Value="{Binding StatusProgress}" Maximum="100" Minimum="0" Height="16" Width="198" />
</StatusBarItem>
</StatusBar>
With the code behind in MainWindow.xaml.cs:
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
public MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext;
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel.Download();
}
And the code in the MainWindowViewModel:
private string _statusMessage = "Downloading something";
public string StatusMessage
{
get => _statusMessage;
set
{
if (value == _statusMessage) return;
_statusMessage = value;
OnPropertyChanged();
}
}
private int _statusProgress;
public int StatusProgress
{
get => _statusProgress;
set
{
if (value == _statusProgress) return;
_statusProgress = value;
OnPropertyChanged();
}
}
public void Download()
{
var dl = new FileDownloader();
dl.ProgressChanged += (sender, args) =>
{
StatusProgress = args.Progress;
};
dl.Download();
}
And finally the code for FileDownloader:
public class ProgressChangedEventArgs
{
public int Progress { get; set; }
}
public class FileDownloader
{
public event EventHandler<ProgressChangedEventArgs> ProgressChanged;
public void Download()
{
for (var i = 0; i < 100; i++)
{
ProgressChanged?.Invoke(this, new ProgressChangedEventArgs{Progress = i});
Thread.Sleep(200);
}
}
}
In the example, the progress bar remains blank, until FileDownloader finishes its loop, and then suddenly the progress bar shows full progress, i.e. complete.
What's happening
Anything that is not about UI should be done in tasks, because, if not, you're blocking the UI thread and the UI.
In your case, the download was happening on you UI thread, the latter was waiting for the download to finish before updating your UI.
Solution
You need to do two things to solve your problem:
remove the work from the UI thread.
make sure the work can communicate with you UI thread.
So, first, start the download work as a Task like this:
private ICommand _startDownloadCommand;
public ICommand StartDownloadCommand
{
get
{
return _startDownloadCommand ?? (_startDownloadCommand = new DelegateCommand(
s => { Task.Run(() => Download()); },
s => true));
}
}
and connect the button to the command like this:
<Button Command="{Binding StartDownloadCommand}" Content="Start download" Height="20"/>
Then have you download method as such:
public void Download()
{
Application.Current.Dispatcher.Invoke(() => { StatusMessage = "download started"; });
var dl = new FileDownloader();
dl.ProgressChanged += (sender, args) =>
{
Application.Current.Dispatcher.Invoke(() => { StatusProgress = args.Progress; });
};
dl.Download();
Application.Current.Dispatcher.Invoke(() => { StatusMessage = "download DONE"; });
}
The dispatch will have your property (on UI thread) updated from a non UI thread.
And yet, the DelegateCommand helper class:
public class DelegateCommand : ICommand
{
private readonly Predicate<object> _canExecute;
private readonly Action<object> _execute;
public event EventHandler CanExecuteChanged;
public DelegateCommand(Action<object> execute)
: this(execute, null) {}
public DelegateCommand(Action<object> execute,
Predicate<object> canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
Remarks
In order to implement the MVVM pattern I had this code behind:
public partial class MainWindow : IView
{
public IViewModel ViewModel
{
get { return (IViewModel)DataContext; }
set { DataContext = value; }
}
public MainWindow()
{
DataContext = new MainWindowViewModel();
}
}
public interface IViewModel {}
public interface IView {}
and this View:
<Window x:Class="WpfApp1.MainWindow"
d:DataContext="{d:DesignInstance local:MainWindowViewModel,
IsDesignTimeCreatable=True}"
xmlns:local="clr-namespace:WpfApp1"
and this ViewModel:
public class MainWindowViewModel: INotifyPropertyChanged, IViewModel
This happens, because StatusBarItem default style sets its HorizontalContentAlignment to Left, which leads ProgressBar to get only a small amount of space horizontally.
You can make the ProgressBar to fill the StatusBarItem completely by setting StatusBarItem's HorizontalContentAlignment to Stretch or you can set the Width of the ProgressBar.
ProgressBar is a DispatcherObject, and DispatcherObject can be only accessed by the Dispatcher it is associated with.
If I understand your question well your OnFileTransferStatusChanged is being triggered on a background thread, so since you're not accessing controls using a Dispatcher (or from the UI thread) you're not guaranteed that the code will work.
The problem is that binding from a non-UI thread usually works until it doesn't - e.g. on a non-dev machine.
like first answer
be sure to be on the main UI thread, because OnFileTransferStatusChanged is on another thread. use this in your event
Application.Current.Dispatcher.Invoke(prio, (ThreadStart)(() =>
{
StatusMessage = fileTransferStatusEventArgs.RelativePath;
StatusProgress = fileTransferStatusEventArgs.Progress;
}));
I made few changes to your sample as your file downloaded is working on UI thread and application just freezes you can see it by changing focus to other application and trying to get back - window will not appear nor update.
changes:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Task.Factory.StartNew(() => ViewModel.Download());
}
forces download to execute in new thread.
public MainWindow()
{
InitializeComponent();
DataContext = ViewModel = new MainWindowViewModel();
}
public MainWindowViewModel ViewModel { get; }
removed cast and access to UI thread only property DataContext.
Now I can see progress bar filling up.
You can't see any changes because your Main thread AKA UI Thread is busy Sleeping
and it does not have time to update your UI
Let Task handle your lengthy job and Main thread for Updating UI
Wrap your code inside Task and you can see your progress bar progressing.
private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
_viewModel.Download();
});
//_viewModel.Download(); //this will run on UI Thread
}

How do I find what text had been added with TextChanged

I'm looking to synchronize between a text in the textbox and string in a variable. I found how to get the index in which the string was changed (in the textbox), the length added and length removed, but how can I actually find the string added?
So far I've used TextChangedEventArgs.Changes, and got the properties of the items in it (ICollection).
I'm trying to create a password box in which I could show the actual password by a function. hence I do not want the textbox to synchronize directly (for example, in the textbox would appear "*****" and in the string "hello").
If you want only text added you can do this
string AddedText;
private void textbox_TextChanged(object sender, TextChangedEventArgs e)
{
var changes = e.Changes.Last();
if (changes.AddedLength > 0)
{
AddedText = textbox.Text.Substring(changes.Offset,changes.AddedLength);
}
}
Edit
If you want all added and remove text you can do this
string oldText;
private void textbox_GotFocus(object sender, RoutedEventArgs e)
{
oldText = textbox.Text;
}
string AddedText;
string RemovedText;
private void textbox_TextChanged(object sender, TextChangedEventArgs e)
{
var changes = e.Changes.Last();
if (changes.AddedLength > 0)
{
AddedText = textbox.Text.Substring(changes.Offset, changes.AddedLength);
if (changes.RemovedLength == 0)
{
oldText = textbox.Text;
RemovedText = "";
}
}
if (changes.RemovedLength > 0)
{
RemovedText = oldText.Substring(changes.Offset, changes.RemovedLength);
oldText = textbox.Text;
if (changes.AddedLength == 0)
{
AddedText = "";
}
}
}
DataBinding is the most common way in WPF to show and collect data in a UI
Try this:
<Window x:Class="WpfApp3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp3"
mc:Ignorable="d"
Title="MainWindow"
Height="350"
Width="525">
<Grid>
<TextBox Text="{Binding Path=SomeText, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left"
Margin="101,83,0,0"
VerticalAlignment="Top"
Width="75" />
<TextBlock Text="{Binding SomeText}"
HorizontalAlignment="Left"
Margin="101,140,0,0"
VerticalAlignment="Top"
Width="75" />
</Grid>
</Window>
Code for the window:
public partial class MainWindow : Window
{
private readonly AViewModel viewModel = new AViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = viewModel;
}
}
And the code for the ViewModel that holds the data you want to show and collect:
public class AViewModel : INotifyPropertyChanged
{
private string someText;
public string SomeText
{
get
{
return someText;
}
set
{
if (Equals(this.someText, value))
{
return;
}
this.someText = value;
this.OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(
[CallerMemberName]string propertyName = null)
{
this.PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(propertyName));
}
}
Although this looks complicated for a simple scenario it has a lot of advantages:
You can write automated (unit)test for the ViewModel without creating a UI
Adding extra fields and logic is trivial
If the UI needs to change, the ViewModel will not always need to change
The core of the mechanism is the {Binding ...} bit in the Xaml that tell WPF to synchronize the data between the Text property of the TextBox and the SomeText property of the object that is assigned to the DataContext.
The other significant bits are:
- in the constructor of the window the setting of the DataContext and
- in the ViewModel the raising of the PropertyChanged event when the SomeText property changes so the binding will be notified.
Note that this is just a basic example of DataBinding, there are many improvements that could be made in this code.

Prism Custom Confirmation Interaction

I created a custom confirmation window in an app with Prism Unity, WPF & Mvvm. I need help with the notifications that need to be sent back to the viewmodel. I have this in the detail record view, let's call it MyDetailView.
<!-- Custom Confirmation Window -->
<ie:Interaction.Triggers>
<interactionRequest:InteractionRequestTrigger
SourceObject="{Binding ConfirmationRequest, Mode=TwoWay}">
<mycontrols:PopupWindowAction1 IsModal="True"/>
</interactionRequest:InteractionRequestTrigger>
</ie:Interaction.Triggers>
As shown above, I made the interaction Mode=TwoWay so that the confirmation popup window can send back the button click result for the OK or Cancel button. The confirmation window appears as it should, but I don't know how to send the button click result back to my viewmodel, say MyDetailViewModel. That is the main question.
EDIT: This is MyDetailViewMmodel method that raises the InteractionRequest.
private void RaiseConfirmation()
{ConfirmationRequest
.Raise(new Confirmation()
{
Title = "Confirmation Popup",
Content = "Save Changes?"
}, c =>{if (c.Confirmed)
{ UoW.AdrTypeRos.Submit();}
This is the PopupWindowAction1 class. Part of the answer to the question may be how do I implement the Notification and FinishedInteraction methods.
class PopupWindowAction1 : PopupWindowAction, IInteractionRequestAware
{
protected override Window GetWindow(INotification notification)
{ // custom metrowindow using mahapps
MetroWindow wrapperWindow = new ConfirmWindow1();
wrapperWindow.DataContext = notification;
wrapperWindow.Title = notification.Title;
this.PrepareContentForWindow(notification, wrapperWindow);
return wrapperWindow;
}
public INotification Notification
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public Action FinishInteraction
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
}
Is there some interaction I need to put in my ConfirmWindow1, something like this?
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewMouseLeftButtonUp">
<ei:CallMethodAction
TargetObject="{Binding RelativeSource={RelativeSource AncestorType=UserControl},
Path=DataContext}"
MethodName="DataContext.ValidateConfirm"/>
</i:EventTrigger>
</i:Interaction.Triggers>
Do I even need to do that type of interaction within the button? If so, how do I code it needs so that it corresponds to the particular viewmodel that invoked the interaction. Any suggestions? Thank you.
Main thing is, when you raise the interaction, provide a callback that is triggered when the interaction is finished. This callback gets the notification back and your interaction should have stored all potentially interesting return values there.
Here's an example...
Relevant parts of the ViewModel:
public InteractionRequest<SelectQuantityNotification> SelectQuantityRequest
{
get;
}
// in some handler that triggers the interaction
SelectQuantityRequest.Raise( new SelectQuantityNotification { Title = "Load how much stuff?", Maximum = maximumQuantity },
notification =>
{
if (notification.Confirmed)
_worldStateService.ExecuteCommand( new LoadCargoCommand( sourceStockpile.Stockpile, cartViewModel.Cart, notification.Quantity ) );
} );
... and from the View:
<i:Interaction.Triggers>
<interactionRequest:InteractionRequestTrigger
SourceObject="{Binding SelectQuantityRequest, Mode=OneWay}">
<framework:FixedSizePopupWindowAction>
<interactionRequest:PopupWindowAction.WindowContent>
<views:SelectSampleDataForImportPopup/>
</interactionRequest:PopupWindowAction.WindowContent>
</framework:FixedSizePopupWindowAction>
</interactionRequest:InteractionRequestTrigger>
</i:Interaction.Triggers>
Additionally, we need a class to hold the data that's passed around, and a ViewModel/View pair for the interaction itself.
Here's the data holding class (note that Maximum is passed to the interaction, and Quantity returned from it):
internal class SelectQuantityNotification : Confirmation
{
public int Maximum
{
get;
set;
}
public int Quantity
{
get;
set;
}
}
This is the View of the interaction popup:
<UserControl x:Class="ClientModule.Views.SelectSampleDataForImportPopup"
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:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel Orientation="Vertical">
<TextBlock>
Amount: <Run Text="{Binding Quantity}"/>
</TextBlock>
<Slider Orientation="Horizontal" Minimum="0" Maximum="{Binding Maximum}" Value="{Binding Quantity}" TickPlacement="BottomRight"/>
<Button Content="Ok" Command="{Binding OkCommand}"/>
</StackPanel>
</UserControl>
and it's ViewModel:
internal class SelectSampleDataForImportPopupViewModel : BindableBase, IInteractionRequestAware
{
public SelectSampleDataForImportPopupViewModel()
{
OkCommand = new DelegateCommand( OnOk );
}
public DelegateCommand OkCommand
{
get;
}
public int Quantity
{
get { return _notification?.Quantity ?? 0; }
set
{
if (_notification == null)
return;
_notification.Quantity = value;
OnPropertyChanged( () => Quantity );
}
}
public int Maximum => _notification?.Maximum ?? 0;
#region IInteractionRequestAware
public INotification Notification
{
get { return _notification; }
set
{
SetProperty( ref _notification, value as SelectQuantityNotification );
OnPropertyChanged( () => Maximum );
OnPropertyChanged( () => Quantity );
}
}
public Action FinishInteraction
{
get;
set;
}
#endregion
#region private
private SelectQuantityNotification _notification;
private void OnOk()
{
if (_notification != null)
_notification.Confirmed = true;
FinishInteraction();
}
#endregion
}

how to show messagebox when wpf app closing?

I have a WPF app with MVVM pattern and it contains following 2 views:
1:MainWindow.xaml (it's a window)
below is main portion of MainWindow.xaml:
<Window.Resources>
<DataTemplate DataType="{x:Type vm:XliffListViewModel}">
<vw:XliffListView />
</DataTemplate>
</Window.Resources>
<Grid Margin="4">
<Border Background="GhostWhite" BorderBrush="LightGray" BorderThickness="1" CornerRadius="5" >
<ItemsControl ItemsSource="{Binding ViewModels}" Margin="4" />
</Border>
</Grid>
2:XliffListView.xaml (it's a user control)
XliffListView contain a datagrid and a button for save all changes that happens
I want to show messagebox when user closing app if changes not saved
You can write and Attached behavior that handles Window.Closing() event of the Window and executes the ClosingCommand from ViewModel and returns true or false as parameter so that one can cancel (e.Cancel) closing event if the VM wants to stop th window from closing.
The code below is just for concept and is not complete
XAML
<Window x:Class="..."
...
myBheaviors:WindowBehaviors.ClosingCommand="{Binding MyClosingCommand}">
...
</Window>
ViewModel
public class MyWindowViewModel
{
public ICommand MyClosingCommand
{
get
{
if (_myClosingCommand == null)
_myClosingCommand
= new DelegateCommand<CancelEventArgs>(OnClosing);
return _myClosingCommand;
}
}
private void OnClosing(CancelEventArgs e)
{
if (this.Dirty) //Some function that decides if the VM has pending changes.
e.Cancel = true;
}
}
Attached Behavior
public static class WindowBehaviors
{
public static readonly DependencyProperty ClosingCommandProperty
= DependencyProperty.RegistsrrAttached(
"ClosingCommand",
...,
new PropertyMetataData(OnClosingCommandChanged);
public static ICommand GetClosingCommand(...) { .. };
public static void SetClosingCommand(...) { .. };
private static void OnClosingCommandChanged(sender, e)
{
var window = sender as Window;
var command = e.NewValue as ICommand;
if (window != null && command != null)
{
window.Closing
+= (o, args) =>
{
command.Execute(args);
if (args.Cancel)
{
MessageBox.Show(
"Window has pending changes. Cannot close");
// Now window will be stopped from closing.
}
};
}
}
}
EDIT:
For user controls, instead of Closing use Unloaded event.
Also try to establish a hierarchy between ViewModels i.e.e Window's ViewModel should contain UserControl's ViewModel. So that Closing event's IsDirty() call can scrutinize the UserControls' ViewModel also.

wpf MVVM confusion

I have been looking at MVVM for the last couple days and thought i would try out a simple example to update a text box with a time. However i'm having a bit of trouble wrapping my head around this. I have a two projects in my Solution.. one i'm calling TimeProvider that right now is just returning Datetime event and another that is called E. Eventually i will use TimeProvider to provide a lot more information but i want to understand something simple first. Can some one please tell me why i'm not geting the gui to update.
namespace E.TimeProvider
{
public interface ITimeSource
{
void Subscribe();
event Action<Time> TimeArrived;
}
}
namespace E.TimeProvider
{
public class Time
{
private DateTime _earthDate;
public Time()
{
}
public Time(DateTime earthDate)
{
this._earthDate = earthDate;
}
public DateTime EarthDate
{
get { return _earthDate; }
set { _earthDate = value; }
}
}
}
namespace E.TimeProvider
{
public class TimeSource : ITimeSource
{
private const int TIMER_INTERVAL = 50;
public event Action<Time> TimeArrived;
private bool subscribe;
public TimeSource()
{
subscribe = false;
Thread timeGenerator = new Thread(new ThreadStart(GenerateTimes));
timeGenerator.IsBackground = true;
timeGenerator.Priority = ThreadPriority.Normal;
timeGenerator.Start();
}
public void Subscribe()
{
if (subscribe)
return;
subscribe = true;
}
private void GenerateTimes()
{
while (true)
{
GenerateAndPublishTimes();
Thread.Sleep(TIMER_INTERVAL);
}
}
private void GenerateAndPublishTimes()
{
DateTime earthDate = DateTime.Now;
Time time = new Time(earthDate);
TimeArrived(time);
}
}
}
Then i have my project E
xaml
<Window x:Class="E.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="200" xmlns:my="clr-namespace:Exiled" WindowStyle="None" WindowStartupLocation="CenterScreen" ResizeMode="NoResize">
<Grid>
<my:TimeControl HorizontalAlignment="Left" x:Name="timeControl1" VerticalAlignment="Top" Height="300" Width="200" />
</Grid>
</Window>
<UserControl x:Class="E.TimeControl"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="200" Background="Black" Foreground="White">
<Grid>
<TextBlock Height="41" HorizontalAlignment="Left" Margin="12,31,0,0" Text="{Binding Path=EarthTime}" VerticalAlignment="Top" Width="176" FontSize="35" TextAlignment="Center" />
</Grid>
</UserControl>
and the rest
namespace E
{
public class TimeControlViewModel : DependencyObject
{
private readonly ITimeSource _source;
public ObservableCollection<TimeViewModel> Times { get; set; }
public TimeControlViewModel()
{
this.Times = new ObservableCollection<TimeViewModel>();
}
public TimeControlViewModel(ITimeSource source)
{
this.Times = new ObservableCollection<TimeViewModel>();
_source = source;
_source.TimeArrived += new Action<Time>(_source_TimeArrived);
}
public void Subscribe()
{
_source.Subscribe();
}
void _source_TimeArrived(Time time)
{
TimeViewModel tvm = new TimeViewModel();
tvm.Time = time;
}
}
}
namespace E
{
class SubscribeCommand
{
private readonly TimeControlViewModel _vm;
public SubscribeCommand(TimeControlViewModel vm)
{
_vm = vm;
}
public void Execute(object parameter)
{
_vm.Subscribe();
}
}
}
namespace E
{
public class TimeViewModel : DependencyObject
{
public TimeViewModel()
{
}
public Time Time
{
set
{
this.EarthDate = value.EarthDate;
}
}
public DateTime EarthDate
{
get { return (DateTime)GetValue(DateProperty); }
set { SetValue(DateProperty, value); }
}
// Using a DependencyProperty as the backing store for Date. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DateProperty =
DependencyProperty.Register("EarthDate", typeof(DateTime), typeof(TimeViewModel), new UIPropertyMetadata(DateTime.Now));
}
}
You've got a few issues here:
You're not seeing an update, because the DataContext of your Window or UserControl is not set to the TimeViewModel instance you create.
Typically, ViewModel instances should not be DependencyObjects. Instead, implement INotifyPropertyChanged. This keeps them from having a dependency on WPF, but still allows them to work properly with MVVM.
I'd recommend reading through a detailed introduction to MVVM, such as the one I wrote here. In particular, you'll need to understand how templating works and the DataContext in order to use binding properly.
For a start it looks like you're binding to EarthTime:
Text="{Binding Path=EarthTime}"
but the property itself is called EarthDate
public DateTime EarthDate

Resources