I created a new WPF Project in VS2017 and also imported MVVM Light via NuGet.
Then I added some code that should change the background color from the MainWindows Grid every 25 milliseconds. Sadly that change doesn'T propagate and I have no clue why it doesn't update. Maybe someone here can help me.
Here is the code:
MainViewModel.cs
using GalaSoft.MvvmLight;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace Strober.ViewModel
{
/// <summary>
/// This class contains properties that the main View can data bind to.
/// <para>
/// Use the <strong>mvvminpc</strong> snippet to add bindable properties to this ViewModel.
/// </para>
/// <para>
/// You can also use Blend to data bind with the tool's support.
/// </para>
/// <para>
/// See http://www.galasoft.ch/mvvm
/// </para>
/// </summary>
public class MainViewModel : ObservableObject
{
private DispatcherTimer timer;
public string Title { get; set; }
private Brush _background;
public Brush Background
{
get
{
return _background;
}
set
{
_background = value;
OnPropertyChanged("Background");
}
}
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel()
{
Background = new SolidColorBrush(Colors.Black);
timer = new DispatcherTimer();
timer.Tick += Timer_Tick;
timer.Interval = new TimeSpan(0, 0, 0,0,100);
timer.Start();
}
private void Timer_Tick(object sender, System.EventArgs e)
{
if (Background == Brushes.Black)
{
Background = new SolidColorBrush(Colors.White);
Title = "White";
}
else
{
Background = new SolidColorBrush(Colors.Black);
Title = "Black";
}
}
#region INotifiedProperty Block
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string PropertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
#endregion
}
}
ViewModelLocator.cs
/*
In App.xaml:
<Application.Resources>
<vm:ViewModelLocator xmlns:vm="clr-namespace:Strober"
x:Key="Locator" />
</Application.Resources>
In the View:
DataContext="{Binding Source={StaticResource Locator}, Path=ViewModelName}"
You can also use Blend to do all this with the tool's support.
See http://www.galasoft.ch/mvvm
*/
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Ioc;
using CommonServiceLocator;
namespace Strober.ViewModel
{
/// <summary>
/// This class contains static references to all the view models in the
/// application and provides an entry point for the bindings.
/// </summary>
public class ViewModelLocator
{
/// <summary>
/// Initializes a new instance of the ViewModelLocator class.
/// </summary>
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
////if (ViewModelBase.IsInDesignModeStatic)
////{
//// // Create design time view services and models
//// SimpleIoc.Default.Register<IDataService, DesignDataService>();
////}
////else
////{
//// // Create run time view services and models
//// SimpleIoc.Default.Register<IDataService, DataService>();
////}
SimpleIoc.Default.Register<MainViewModel>();
}
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
public static void Cleanup()
{
// TODO Clear the ViewModels
}
}
}
MainWindow.xaml (MainWindow.xaml.cs is just the regularly generated file)
<Window x:Class="Strober.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:Strober"
mc:Ignorable="d"
DataContext="{Binding Main, Source={StaticResource Locator}}"
Title="{Binding Title}" Height="450" Width="800">
<Grid Background="{Binding Background}">
</Grid>
</Window>
App.xaml
<Application x:Class="Strober.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Strober" StartupUri="MainWindow.xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" d1p1:Ignorable="d" xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006">
<Application.Resources>
<ResourceDictionary>
<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" xmlns:vm="clr-namespace:Strober.ViewModel" />
</ResourceDictionary>
</Application.Resources>
</Application>
greg
The primary issue in your code is that System.Windows.Media.SolidColorBrush
does not override the Equals()
method, and so your expression Background == Brushes.Black
is never true
. Since you are creating explicit new instances of the SolidColorBrush
object, and since the ==
operator is just comparing the instance references, the comparison between your brush value and the built-in Brushes.Black
instance always fails.
The easiest way to fix the code would be to just use the actual Brushes
instances:
private void Timer_Tick(object sender, System.EventArgs e)
{
if (Background == Brushes.Black)
{
Background = Brushes.White;
Title = "White";
}
else
{
Background = Brushes.Black;
Title = "Black";
}
}
Then when you compare the instance references, they are in fact comparable, and you will detect the "black" condition as desired.
I'll note that since you also aren't raising PropertyChanged
for changes to the Title
property, that binding also will not work as expected.
For what it's worth, I would avoid your design altogether. First, view model objects should avoid using UI-specific types. For sure, this would include the Brush
type. Arguably, it also includes the DispatcherTimer
, since that exists in service of the UI. Besides which, DispatcherTimer
is a relatively imprecise timer, and while it exists primarily to have a timer that raises its Tick
event on the dispatcher thread that owns the timer, since WPF automatically marshals property-change events from any other thread to the UI thread, it's not nearly as useful in this example.
Here is a version of your program that IMHO is more in line with typical WPF programming practices:
class MainViewModel : NotifyPropertyChangedBase
{
private string _title;
public string Title
{
get { return _title; }
set { _UpdateField(ref _title, value); }
}
private bool _isBlack;
public bool IsBlack
{
get { return _isBlack; }
set { _UpdateField(ref _isBlack, value, _OnIsBlackChanged); }
}
private void _OnIsBlackChanged(bool obj)
{
Title = IsBlack ? "Black" : "White";
}
public MainViewModel()
{
IsBlack = true;
_ToggleIsBlack(); // fire and forget
}
private async void _ToggleIsBlack()
{
while (true)
{
await Task.Delay(TimeSpan.FromMilliseconds(100));
IsBlack = !IsBlack;
}
}
}
This view model class uses a base class that I use for all my view models, so I don't have to reimplement INotifyPropertyChanged
all the time:
class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void _UpdateField<T>(ref T field, T newValue,
Action<T> onChangedCallback = null,
[CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
{
return;
}
T oldValue = field;
field = newValue;
onChangedCallback?.Invoke(oldValue);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
You'll notice that the view model class doesn't have any UI-specific behavior. It would work with any program, WPF or otherwise, as long as that program has a way to react to PropertyChanged
events, and to make use of the values in the view model.
To make this work, the XAML gets somewhat more verbose:
<Window x:Class="TestSO55437213TimerBackgroundColor.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:p="http://schemas.microsoft.com/netfx/2007/xaml/presentation"
xmlns:l="clr-namespace:TestSO55437213TimerBackgroundColor"
mc:Ignorable="d"
Title="{Binding Title}" Height="450" Width="800">
<Window.DataContext>
<l:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.Style>
<p:Style TargetType="Grid">
<Setter Property="Background" Value="White"/>
<p:Style.Triggers>
<DataTrigger Binding="{Binding IsBlack}" Value="True">
<Setter Property="Background" Value="Black"/>
</DataTrigger>
</p:Style.Triggers>
</p:Style>
</Grid.Style>
</Grid>
</Window>
(Note: I have explicitly named the http://schemas.microsoft.com/netfx/2007/xaml/presentation XML namespace, for use with the <Style/>
element, solely as a work-around for Stack Overflow's insufficient XML markup handling, which would not otherwise recognize the <Style/>
element as an actual XML element. In your own program, you may feel free to leave that out.)
The key here is that the entire handling of the UI concern is in the UI declaration itself. The view model doesn't need to know how the UI represents the colors black or white. It just toggles a flag. Then the UI monitors that flag, and applies property setters as appropriate to its current value.
Finally, I'll note that for repeatedly-changing state in the UI like this, another approach is to use WPF's animation features. That's beyond the scope of this answer, but I encourage you to read about it. One advantage to doing so is that the animation uses an even higher-resolution timing model than the thread-pool based Task.Delay()
method I use above, and so would generally provide an even smoother animation (though, certainly as your interval gets smaller and smaller — such as 25ms as your post indicates you intended to use — you will have trouble getting WPF to keep up smoothly regardless; at some point, you'll find that the higher-level UI frameworks like WinForms, WPF, Xamarin, etc. just can't operate at such a fine-grained timer level).