How do I determine inactivity in a MVVM application? - wpf

I have an MVVM kiosk application that I need to restart when it has been inactive for a set amount of time. I'm using Prism and Unity to facilitate the MVVM pattern. I've got the restarting down and I even know how to handle the timer. What I want to know is how to know when activity, that is any mouse event, has taken occurred. The only way I know how to do that is by subscribing to the preview mouse events of the main window. That breaks MVVM thought, doesn't it?
I've thought about exposing my window as an interface that exposes those events to my application, but that would require that the window implement that interface which also seems to break MVVM.

Another option is to use the Windows API method GetLastInputInfo.
Some cavets
I'm assuming Windows because it's WPF
Check if your kiosk supports GetLastInputInfo
I don't know anything about MVVM. This method uses a technique that is UI agnostic, so I would think it would work for you.
Usage is simple. Call UserIdleMonitor.RegisterForNotification. You pass in a notification method and a TimeSpan. If user activity occurs and then ceases for the period specified, the notification method is called. You must re-register to get another notification, and can Unregister at any time. If there is no activity for 49.7 days (plus the idlePeriod), the notification method will be called.
public static class UserIdleMonitor
{
static UserIdleMonitor()
{
registrations = new List<Registration>();
timer = new DispatcherTimer(TimeSpan.FromSeconds(1.0), DispatcherPriority.Normal, TimerCallback, Dispatcher.CurrentDispatcher);
}
public static TimeSpan IdleCheckInterval
{
get { return timer.Interval; }
set
{
if (Dispatcher.CurrentDispatcher != timer.Dispatcher)
throw new InvalidOperationException("UserIdleMonitor can only be used from one thread.");
timer.Interval = value;
}
}
public sealed class Registration
{
public Action NotifyMethod { get; private set; }
public TimeSpan IdlePeriod { get; private set; }
internal uint RegisteredTime { get; private set; }
internal Registration(Action notifyMethod, TimeSpan idlePeriod)
{
NotifyMethod = notifyMethod;
IdlePeriod = idlePeriod;
RegisteredTime = (uint)Environment.TickCount;
}
}
public static Registration RegisterForNotification(Action notifyMethod, TimeSpan idlePeriod)
{
if (notifyMethod == null)
throw new ArgumentNullException("notifyMethod");
if (Dispatcher.CurrentDispatcher != timer.Dispatcher)
throw new InvalidOperationException("UserIdleMonitor can only be used from one thread.");
Registration registration = new Registration(notifyMethod, idlePeriod);
registrations.Add(registration);
if (registrations.Count == 1)
timer.Start();
return registration;
}
public static void Unregister(Registration registration)
{
if (registration == null)
throw new ArgumentNullException("registration");
if (Dispatcher.CurrentDispatcher != timer.Dispatcher)
throw new InvalidOperationException("UserIdleMonitor can only be used from one thread.");
int index = registrations.IndexOf(registration);
if (index >= 0)
{
registrations.RemoveAt(index);
if (registrations.Count == 0)
timer.Stop();
}
}
private static void TimerCallback(object sender, EventArgs e)
{
LASTINPUTINFO lii = new LASTINPUTINFO();
lii.cbSize = Marshal.SizeOf(typeof(LASTINPUTINFO));
if (GetLastInputInfo(out lii))
{
TimeSpan idleFor = TimeSpan.FromMilliseconds((long)unchecked((uint)Environment.TickCount - lii.dwTime));
//Trace.WriteLine(String.Format("Idle for {0}", idleFor));
for (int n = 0; n < registrations.Count; )
{
Registration registration = registrations[n];
TimeSpan registeredFor = TimeSpan.FromMilliseconds((long)unchecked((uint)Environment.TickCount - registration.RegisteredTime));
if (registeredFor >= idleFor && idleFor >= registration.IdlePeriod)
{
registrations.RemoveAt(n);
registration.NotifyMethod();
}
else n++;
}
if (registrations.Count == 0)
timer.Stop();
}
}
private static List<Registration> registrations;
private static DispatcherTimer timer;
private struct LASTINPUTINFO
{
public int cbSize;
public uint dwTime;
}
[DllImport("User32.dll")]
private extern static bool GetLastInputInfo(out LASTINPUTINFO plii);
}
Updated
Fixed issue where if you tried to re-register from the notification method you could deadlock.
Fixed unsigned math and added unchecked.
Slight optimization in timer handler to allocate notifications only as needed.
Commented out the debugging output.
Altered to use DispatchTimer.
Added ability to Unregister.
Added thread checks in public methods as this is no longer thread-safe.

You could maybe use MVVM Light's EventToCommand behaviour to link the MouseMove/MouseLeftButtonDown event to a command. This is normally done in blend because it's really easy.
Here's some example xaml if you don't have blend:
<Grid>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding theCommand} />
</i:EventTrigger>
</i:Interaction.Triggers>
</Grid>
Where i: is a xml namespace for Blend.Interactivity.

This is not an official answer, but here is my version of UserIdleMonitor for anyone who is interested:
public class UserIdleMonitor
{
private DispatcherTimer _timer;
private TimeSpan _timeout;
private DateTime _startTime;
public event EventHandler Timeout;
public UserIdleMonitor(TimeSpan a_timeout)
{
_timeout = a_timeout;
_timer = new DispatcherTimer(DispatcherPriority.Normal, Dispatcher.CurrentDispatcher);
_timer.Interval = TimeSpan.FromMilliseconds(100);
_timer.Tick += new EventHandler(timer_Tick);
}
public void Start()
{
_startTime = new DateTime();
_timer.Start();
}
public void Stop()
{
_timer.Stop();
}
private void timer_Tick(object sender, EventArgs e)
{
LASTINPUTINFO lii = new LASTINPUTINFO();
lii.cbSize = Marshal.SizeOf(typeof(LASTINPUTINFO));
if (GetLastInputInfo(out lii))
{
TimeSpan idleFor = TimeSpan.FromMilliseconds((long)unchecked((uint)Environment.TickCount - lii.dwTime));
TimeSpan aliveFor = TimeSpan.FromMilliseconds((long)unchecked((uint)Environment.TickCount - _startTime.Millisecond));
Debug.WriteLine(String.Format("aliveFor = {0}, idleFor = {1}, _timeout = {2}", aliveFor, idleFor, _timeout));
if (aliveFor >= idleFor && idleFor >= _timeout)
{
_timer.Stop();
if (Timeout != null)
Timeout.Invoke(this, EventArgs.Empty);
}
}
}
#region Win32 Stuff
private struct LASTINPUTINFO
{
public int cbSize;
public uint dwTime;
}
[DllImport("User32.dll")]
private extern static bool GetLastInputInfo(out LASTINPUTINFO plii);
#endregion
}

The only way I know how to do that is by subscribing to the preview mouse events of the main window. That breaks MVVM thought, doesn't it?
That really depends on how you do it.
You could pretty easily write a Behavior or an Attached Property that you hook into this event and use it to trigger an ICommand in your ViewModel. This way, you're basically pushing a "Something happened" event down to the VM, where you can handle this completely in your business logic.

Related

How can I run a Dispatcher.Invoke() in a .NET Core 3 class library?

I am developing a WPF/NetCore3.1 application with MVVM. In the view there is a button which is bound to a RelayCommand. The ViewModel is in a different class library than the View. In the ViewModel a timer is started which increments a variable every second and triggers the CanExecuteChanged event of the RelayCommand.
Here is my ViewModel:
public ImportExportViewModel()
{
MakeOfferCommand = new RelayCommand(MakeOffer, CanMakeOffer);
Timer t = new Timer(1000);
t.Elapsed += T_Elapsed;
t.Start();
}
private void T_Elapsed(object sender, ElapsedEventArgs e)
{
ElapsedTime++;
MakeOfferCommand.RaiseCanExecuteChanged();
}
private void MakeOffer()
{
// TODO Make Offer
}
private bool CanMakeOffer()
{
return ElapsedTime < 60;
}
And here the RaiseCanExecuteChanged:
public void RaiseCanExecuteChanged()
{
var handler = CanExecuteChanged;
if (handler != null)
{
handler(this, new EventArgs());
}
}
But here I get an InvalidOperationException: the calling thread cannot access this object because the object is owned by another thread.
Normally I would execute a Dispatcher.Invoke() here, but that seems not to exist in .NetCore3.1.
Can anyone tell me how I can still make cross-thread calls?
You could inject your view model with an IDispatch interface that you implement in each platform:
Interface:
public interface IDispatch
{
bool CheckAccess();
void Invoke(Action action);
}
View Model:
public IDispatch Dispatch { get; set; }
private void T_Elapsed(object sender, ElapsedEventArgs e)
{
if (Dispatch != null && !Dispatch.CheckAccess())
Dispatch.Invoke(new Action(() => { /* do something */ }));
...
}
WPF implementation:
public class WpfDispatch : IDispatch
{
private readonly Dispatcher _dispatcher;
public WpfDispatch(Dispatcher dispatcher) =>
_dispatcher = dispatcher;
public bool CheckAccess() => _dispatcher.CheckAccess();
public void Invoke(Action action) => _dispatcher.Invoke(action);
}

Is there a more efficient way to read a value from Serial Port and Update a Chart in realtime - WPF

Using Live Charts, I am creating a realtime graph which updates with values read in from the serial port. Now I can get this to work but I don't think I am doing this as efficiently as I could as I am inexperienced using C# and WPF.
I am storing the data I am reading in from the serial port in a SerialCommunication class. I am then using a button to start a new task which opens the serial port and updates my graph.
My issue is that I want to be able to update the graph everytime the Serial class receives a new value, however, my chart is updated in the Read() function which is called from starting a new task and I feel this may cause threading issues.
Any advice would be appreciated.
Serial class
public class SerialCommunication
{
private string _value;
SerialPort serialPort = null;
public SerialCommunication()
{
InitializeComms();
}
private void InitializeComms()
{
try
{
serialPort = new SerialPort("COM6", 115200, Parity.None, 8, StopBits.One); // Update this to avoid hard coding COM port and BAUD rate
serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
}
catch (Exception ex) { MessageBox.Show(ex.Message); }
}
~SerialCommunication()
{
if(serialPort.IsOpen)
serialPort.Close();
}
public void ReceiveData()
{
try
{
if (!serialPort.IsOpen)
serialPort.Open();
}
catch(Exception ex) { MessageBox.Show(ex.Message); }
}
public void StopReceivingData()
{
try
{
if (serialPort.IsOpen)
serialPort.Close();
}
catch(Exception ex) { MessageBox.Show(ex.Message); }
}
public event EventHandler DataReceived;
private void OnDataReceived(EventArgs e)
{
DataReceived?.Invoke(this, e);
}
// read the data in the DataReceivedHandler
// Event Handler
public void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
try
{
SerialPort sp = (SerialPort)sender;
_value = sp.ReadLine();
}
catch (Exception ex) { MessageBox.Show(ex.Message); }
OnDataReceived(EventArgs.Empty);
}
}
Time of Flight class which updates chart from sensor values read from serial port, using code taken from LiveCharts by beto-rodriguez
public TimeOfFlight()
{
InitializeComponent();
// attach an event handler to update graph
serial.DataReceived += new EventHandler(UpdateChart);
// Use PlotData class for graph data which will use this config every time
var mapper = Mappers.Xy<PlotData>()
.X(model => model.DateTime.Ticks)
.Y(model => model.Value);
// Save mapper globally
Charting.For<PlotData>(mapper);
chartValues = new ChartValues<PlotData>();
//lets set how to display the X Labels
XFormatter = value => new DateTime((long)value).ToString("hh:mm:ss");
YFormatter = x => x.ToString("N0");
//AxisStep forces the distance between each separator in the X axis
AxisStep = TimeSpan.FromSeconds(1).Ticks;
//AxisUnit forces lets the axis know that we are plotting seconds
//this is not always necessary, but it can prevent wrong labeling
AxisUnit = TimeSpan.TicksPerSecond;
SetAxisLimits(DateTime.Now);
//ZoomingMode = ZoomingOptions.X;
IsReading = false;
DataContext = this;
}
public ChartValues<PlotData> chartValues { get; set; }
public Func<double, string> XFormatter { get; set; }
public Func<double, string> YFormatter { get; set; }
public double AxisStep { get; set; }
public double AxisUnit { get; set; }
public double AxisMax
{
set
{
_axisXMax = value;
OnPropertyChanged("AxisMax");
}
get { return _axisXMax; }
}
public double AxisMin
{
set
{
_axisXMin = value;
OnPropertyChanged("AxisMin");
}
get { return _axisXMin; }
}
public bool IsReading { get; set; }
private void StartStopGraph(object sender, RoutedEventArgs e)
{
IsReading = !IsReading;
if (IsReading)
{
serial.ReceiveData();
}
else
serial.StopReceivingData();
}
public void UpdateChart(object sender, EventArgs e) // new task
{
try
{
var now = DateTime.Now;
// can chartValues.Add be called from INotifyPropertyChanged in
// SerialCommunication.cs and would this cause an issue with the
chartValues.Add(new PlotData
{
DateTime = now,
Value = 0 // update this
});
SetAxisLimits(now);
//lets only use the last 150 values
if (chartValues.Count > 1000) chartValues.RemoveAt(0);
}
catch (Exception ex) { MessageBox.Show(ex.Message); }
}
private void SetAxisLimits(DateTime now)
{
AxisMax = now.Ticks + TimeSpan.FromSeconds(1).Ticks; // lets force the axis to be 1 second ahead
AxisMin = now.Ticks - TimeSpan.FromSeconds(8).Ticks; // and 8 seconds behind
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
PlotData class
public class PlotData
{
public DateTime DateTime { get; set; }
public int Value { get; set; }
}
Yeah, not the best way IMO.
First of all I wouldn't put anything to do with INotifyPropertyChanged or business/view logic in your SerialCommunication class, from an architectural perspective all it should do manage the opening and closing of the serial device, and any data that does arrive should be passed on other parts of your app via an event.
Secondly, why are you using a loop? You've already subscribed to DataReceived, so just read the data in your DataReceivedHandler and pass it on to said event. That handler will be called on a different thread, but the handlers that subscribed to your event can dispatch to the main GUI thread if they need to.
Which brings me to the third point: if you're data binding correctly (as opposed to updating your controls directly) then you shouldn't need to, just update your data values and leave it to the WPF data binding engine to update as needed.
In a real application you may want to do the updates at a certain interval, and for that you'll want to push your data values to a queue and then process them all at once. The correct way to do this is in C# with an asynchronous Task. Don't use threads, and definitely don't use Thread.Sleep....threads are an outdated technology which has no place in C# except for a few very specific circumstances.
The cleanest way would be to implement your serial communications as a standalone module using Reactive Extensions (IObservable).
That way, your WPF application could subscribe to the message observable on an appropriate threading context, and update the UI each time a new message is received.

Execute Method from Codehind using ViewModel WPF

I've abandoned the MVVM midway through app development just to get this app out.
I've written a method in the code behind to update the database/datagrid etc.
My application navigation is using Commands to the ViewModel firing some event but never touches the code-behind except one time to initialize the class.
So basically I push the button one time and it works with the default initial setting but I can't call my code-behind Update() method anymore once the view as been intialized.
How can I call this code-behind method from the view model?
Thanks!!
Update code
//Navigation ViewModel
//PaneVm.cs
public CommandExtension NewAssignmentCommand { get; set; }
private void CreateCommands()
{
NewAssignmentCommand = new CommandExtension(NewAssignment, CanNewAssignment);
}
GlobalCommands.NewAssignmentCommand = NewAssignmentCommand;
private bool CanNewGroupAssignment(object obj)
{
return true;
}
private void NewGroupAssignment(object obj)
{
OnPropertyChanged("NewGroupAssignmentCommand");
}
//MainVM.cs
// [Events]
void _PaneVm_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "NewGroupAssignmentCommand")
WorkspaceVm.CurrentVm = new NewAssignmentsVm();
}
//NewAssignmentVm.cs
//Constructor
public NewAssignmentsVm()
{
var rc = new RepositoryContext();
_RoResearchers = new ObservableCollection<Researcher>(rc.ResearcherData.GetAllResearchers());
_QuarterDateTime = DateTime.Now;
CreateCommands();
}
//NewAssignment.cs
//Code-behind
//The method
private void UpdateGrid()
{
report_datagrid.ItemsSource = null;
using (var rc = new RepositoryContext())
{
if (quarter_datepicker.SelectedDate != null)
{
if (!String.IsNullOrEmpty(reportType))
researchers = rc.ResearcherData.GetResearchersWeeksByQuarter(Convert.ToDateTime(quarter_datepicker.SelectedDate), reportType).ToList();
}
}
}
UPDATE 2:
I solved my problem based off this answer. I created a Global Action
public static class GlobalCommands
{
public static Action UpdateGrid { get; set; }
}
Then in my code-behind constructor I set the value public
MyCodeBehind()
{
GlobalCommands.UpdateGrid = new Action(() => this.UpdateGrid());
}
Didn't need to bind to the context again. Everything else was the same. Thank you
Main idea is:
class MyCodeBehind
{
public MyCodeBehind()
{
Action action = new Action(()=> this.SomeMethodIWantToCall());
var myVM = new MyVM(action); // This is your ViewModel
this.DataContext = myVM;
}
private void SomeMethodIWantToCall(){...}
}
class MyVM
{
private Action action;
public MyVM(Action someAction)
{
this.action = someAction;
}
private void SomeMethodInVM()
{
this.action(); // Calls the method SomeMethodIWantToCall() in your code behind
}
}
Instead of letting code-behind know about viewmodel, You can make use of NotifyOnSourceUpdated in your xaml binding.
Something like this:
<TextBlock Grid.Row="1" Grid.Column="1" Name="RentText"
Text="{Binding Path=Rent, Mode=OneWay, NotifyOnTargetUpdated=True}"
TargetUpdated="OnTargetUpdated"/>
Here, 'OnTargetUpdated' is a handler in your code behind. This handler will be invoked when "Rent" property of ViewModel is changed.
Details at MSDN

BackgroundWorker not raising RunWorkerCompleted event

In my application's Business Logic layer I have the following classes:
public class EocMonitor : DeviceMonitor {
public BackgroundWorker BackendWorker { get; set; }
public BackgroundWorker EocWorker { get; set; }
public EocMonitor() {
BackendWorker = new BackgroundWorker {
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
BackendWorker.DoWork += BackendWorker_DoWork;
EocWorker = new BackgroundWorker {
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
EocWorker.DoWork += EocWorker_DoWork;
}
private void BackendWorker_DoWork( object sender, DoWorkEventArgs e ) {
// Does some lengthy operation
}
void EocWorker_DoWork( object sender, DoWorkEventArgs e ) {
// Does some lengthy operation
}
public void GetDiagnostics() {
BackendWorker.RunWorkerAsync( new DiagnosticsInfo() );
EocWorker.RunWorkerAsync( new DiagnosticsInfo() );
}
}
public class DiagnosticsInfo {
public int DataTypeCount { get; set; }
public int DataTypesProcessed { get; set; }
}
The BackgroundWorkers are used to query information over the wire from 2 other processes running in my application. The responses can take a while to come back. Plus the data can take a while to come back.
I have a WPF UserControl in my application's main window called Dashboard. The Dashboard has a DataGrid on it that displays the results of the lengthy operations. Because they are lengthy, it also has a Button on it called Refresh that starts the process off. And, because it can take a long time to run, there's a UserControl I wrote called a ProgressControl on the form. This consists of a Cancel Button, a ProgressBar, and a TextBlock where messages can be displayed. When the user clicks on the Cancel Button, the refresh stops.
Here's some code from Dashboard:
public partial class Dashboard : UserControl {
public Dashboard() {
InitializeComponent();
}
private Dashboard_Loaded( object sender, RoutedEventArgs e ) {
if ( !setupProgress && EocMonitor != null ) {
EocMonitor.BackendWorker.ProgressChanged += BackendWorker_ProgressChanged;
EocMonitor.BAckendWorker.RunWorkerCompleted += BackendWorker_RunWorkerCompleted;
EocMonitor.EocWorker.ProgressChkanged += EocWorker_ProgresChanged;
EocMonitor.EocWorker.RunWorkerCompleted += EocWorker_RunWorkerCompleted;
}
}
private void BackendWorker_ProgressChanged( object sender, ProgressChangedEventArgs e ) {
DiagnosticsInfo info = e.UserState as DiagnosticsInfo;
// Other processing to notify the user of the progress
}
private void BackendWorker_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e ) {
// Processing to do when the back-ground worker is finished
}
private void DiagnosticsProgressCtrl_Click( object sender, RoutedEventArgs e ) {
EocMonitor.BackendWorker.CancelAsync();
EocMonitor. EocWorker.CancelAsync();
DiagnosticsProgressCtrl.Visibility = Visibility.Collapsed;
e.Handled = true;
}
void EocWorker_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e ) {
// Processing to do when the back-ground worker is finished
}
private void RefreshButton_Click( object sender, RoutedEventArgs e ) {
DiagnosticsProgressCtrl.Maximum = DiagnosticsProgressCtrl.Minimum = DiagnosticsProgressCtrl.Value = 0.0;
DiagnosticsProgressCtrl.Visibility = Visibility.Visible;
backendDataTypeCount = eocDataTypeCount = 0;
backendWorkerCompleted = eocWorkerCompleted = false;
EocMonitor.GetDiagnostics();
e.Handled = true;
}
}
The problem is that I have placed breakpoints in the DoWork methods and watched them run to completion, yet the RunWorkerCompleted methods are not being called. No errors are occurring or being thrown. This thing is the EocMonitor class and the Dashboard class are in two different DLLs. Does that make a difference? As far as I know it shouldn't, but I don't understand why the completed event handlers aren't getting called. Should I instantiate the BackgroundWorkers in the front-end application?
Tony
The event is raised, but you don't see it because you didn't subscribe to the RunWorkerCompleted event...
BackendWorker.RunWorkerCompleted += BackendWorker_RunWorkerCompleted;
EocWorker.RunWorkerCompleted += EocWorker_RunWorkerCompleted;
Well, after I posted the above, I went back and changed things a bit. I now instantiate the BackgroundWorker objects in the Dashboard control and pass them to the EocMonitor's GetDiagnostics method. The properties in EocMonitor that hold these objects have private setters, so the only way to use them is to create them & pass them to that method. The code in the Dashboard_Loaded is now moved in the RefreshButton_Click method and runs after the objects are instantiated, before they're passed to GetDiagnostics.
This all works now! I see the Progress_Changed methods and the RunWorkerCompleted methods run.
It just hit me why it's probably not working. The EocMonitor object is created on a non UI thread during my program's initalization phase. Since it's calling methods in a UI object, the methods probably can't be called. An Invalid operation exception of some sort is probably being thrown, but there's no place to catch it.
So let that be a lesson: The BackgroundWorker has to be instantiated in code on the UI thread.

How to Notify That View Should Get New Value of Calculated Field

I am working on a WP7 app that displays some times on one page. I have a code behind that has an ObservableCollection of objects. Each object has a calculated property that uses DateTime.Now to determine the time that's displayed on the page. I can't figure out how to "notify" that the property has changed since the property doesn't change, the current time is changing (just once per second). Any ideas? Here's the jist of what I've got:
//my business object
public class Widget
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
private DateTime? _start;
public DateTime? Start
{
get { return _start; }
set { _start = value; }
}
public TimeSpan? TimeSinceStart
{
get { return Start.HasValue ? DateTime.Now - Start.Value : default(TimeSpan); }
}
}
//my viewmodel
public class WidgetDisplayerViewModel : BaseViewModel
{
public WidgetDisplayerViewModel()
{
TimeUpdateTimer.Tick += new EventHandler(TimeUpdateTimer_Tick);
TimeUpdateTimer.Interval = new TimeSpan(0, 0, 1);
TimeUpdateTimer.Start();
}
public WidgetDisplayerViewModel(string selectedCategory) : this()
{
Category = MockDataService.GetCategory(selectedCategory);
Category.Widgets = MockDataService.GetWidgets(selectedCategory).ToObservableCollection();
}
public DispatcherTimer TimeUpdateTimer = new DispatcherTimer();
private DateTime _currentTime;
public DateTime CurrentTime
{
get { return _currentTime; }
set {
_currentTime = value;
NotifyPropertyChanged("CurrentTime");
}
}
public Category Category { get; set; }
void TimeUpdateTimer_Tick(object sender, EventArgs e)
{
CurrentTime = DateTime.Now;
}
}
And then the view is very simple and just needs to display the CurrentTime and then for each Widget in the collection it needs to show the TimeSinceStart. The CurrentTime is getting updated each second by the timer and that gets propogated to the view. That one is easy because the timer is setting it and so I have a chance to call NotifyPropertyChanged("CurrentTime"), but how would I "notify" that all of the TimeSinceStart getters should be called to update the calculated value for each Widget since I'm not setting them?
Thanks!
You'll have to manually refresh the property one way or another. I see you already have a timer ticking every second. So I can suggest you two solutions:
1/ Define a "UpdateTime" method in the Widget object. In this method, call NotifyPropertyChanged("TimeSinceStart"). When the timer is ticking, enumerate the list of widgets, and call the UpdateTime method on each.
2/ Create a global object implementing the INotifyPropertyChanged interface, and holding the value of CurrentTime. Make each of your Widget objects subscribe to the PropertyChanged event of this global class to know when the time is updated. Then, when the event is triggered, call NotifyPropertyChanged("TimeSinceStart").
This can be a tricky one to work out and it can get very messy very fast.
I would suggest you stick with your current approach of having only one timer which is initialised in the main viewmodel. You then have to ask yourself the question - does the age (TimeSinceStart) of the Widget belong on the Widget, or is it purely for display/informational purposes? Is it a core piece of information that each Widget must keep during its lifespan?
This looks to me like it is for display purposes only. So my suggestion is this: once you have called GetWidgets, you could enumerate through each Widget and wrap it in a thin viewmodel of its own. The constructor for that viewmodel takes two parameters - the timer from the main viewmodel, and the Widget. You then subscribe to the timer's Tick event, and from that you notify that the TimeSinceStart property has changed.
public class WidgetWrapper : INotifyPropertyChanged
{
public WidgetWrapper(DispatcherTimer timer, Widget widget)
{
_widget = widget;
timer.Tick += TimerTick;
}
private void TimerTick(object sender, EventArgs e)
{
OnPropertyChanged("TimeSinceStart");
}
public Widget Widget { get { return _widget; } }
public TimeSpan? TimeSinceStart
{
get { return _widget.Start.HasValue ? DateTime.Now - _widget.Start.Value : default(TimeSpan); }
}
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
private readonly Widget _widget;
}
public class WidgetDisplayerViewModel : BaseViewModel
{
public WidgetDisplayerViewModel(string selectedCategory) : this()
{
Category = MockDataService.GetCategory(selectedCategory);
var wrappedWidgets = new ObservableCollection<WidgetWrapper>();
MockDataService.GetWidgets(selectedCategory).ForEach(widget => wrappedWidgets.Add(new WidgetWrapper(TimeUpdateTimer, widget)));
Category.Widgets = wrappedWidgets;
}
}
Wrapping a DTO (entity, Data Transfer Object) with its own viewmodel is a quite common approach when adding functionality to an entity. If you use this appoach you will have to slightly modify any UI bindings that were targetting properties on the Widget, as those UI elements will now be dealing with a WidgetWrapper (or you can just surface the required properties in the WidgetWrapper itself, then no bindings have to change).
Invoke the NotifyPropertyChanged method for the specified property.
public DateTime CurrentTime
{
get { return _currentTime; }
set {
_currentTime = value;
NotifyPropertyChanged("CurrentTime");
NotifyPropertyChanged("TimeSinceStart");
}
}
Subscribe all widgets to CurrentTime PropertyChanged event in Widget constructor
private Widget()
{
App.ViewModel.PropertyChanged += (s, e) =>
{
if (e.PropertyName.Equals("CurrentTime")
{
NotifyPropertyChanged("TimeSinceStart");
}
};
}

Resources