背景颜色绑定未从 Dispatcher Timer 更新
Background Color Binding not updating from Dispatcher Timer
我在 VS2017 中创建了一个新的 WPF 项目,还通过 NuGet 导入了 MVVM Light。
然后我添加了一些代码,每 25 毫秒从 MainWindows Grid 更改背景颜色。可悲的是,这种变化没有传播,我不知道为什么它没有更新。也许这里有人可以帮助我。
代码如下:
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只是定期生成的文件)
<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>
格雷格
您的代码中的主要问题是 System.Windows.Media.SolidColorBrush
没有覆盖 Equals()
方法,因此您的表达式 Background == Brushes.Black
永远不会是 true
。由于您正在创建 SolidColorBrush
对象的显式新实例,并且由于 ==
运算符只是比较实例引用,因此您的画笔值与内置 Brushes.Black
实例之间的比较总是失败。
修复代码的最简单方法是只使用实际的 Brushes
个实例:
private void Timer_Tick(object sender, System.EventArgs e)
{
if (Background == Brushes.Black)
{
Background = Brushes.White;
Title = "White";
}
else
{
Background = Brushes.Black;
Title = "Black";
}
}
那么当你比较实例引用时,它们实际上是可比较的,你会根据需要检测"black"条件。
我会注意到,由于您也没有为 Title
属性 的更改提出 PropertyChanged
,因此绑定也不会按预期工作。
无论如何,我会完全避免使用您的设计。首先,视图模型对象应避免使用 UI 特定类型。当然,这将包括 Brush
类型。可以说,它还包括 DispatcherTimer
,因为它存在于 UI 的服务中。除此之外,DispatcherTimer
是一个相对不精确的计时器,虽然它的存在主要是为了让一个计时器在拥有该计时器的调度程序线程上引发其 Tick
事件,因为 WPF 会自动封送 属性 - 将事件从任何其他线程更改为 UI 线程,在本例中几乎没有用处。
恕我直言,这是一个更符合典型 WPF 编程实践的程序版本:
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;
}
}
}
此视图模型 class 使用我用于所有视图模型的基础 class,因此我不必一直重新实现 INotifyPropertyChanged
:
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));
}
}
您会注意到视图模型 class 没有任何 UI 特定的行为。它适用于任何程序,WPF 或其他程序,只要该程序能够对 PropertyChanged
事件作出反应,并利用视图模型中的值。
为了完成这项工作,XAML 变得更加冗长:
<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>
(注意:我已经明确命名了 http://schemas.microsoft.com/netfx/2007/xaml/presentation XML 命名空间,用于 <Style/>
元素,仅作为 Stack Overflow 不足 XML 标记处理,否则不会将 <Style/>
元素识别为实际的 XML 元素。在您自己的程序中,您可以随意将其省略。)
这里的关键是 UI 关注点的整个处理都在 UI 声明本身。视图模型不需要知道 UI 如何表示黑色或白色。它只是切换一个标志。然后 UI 监视该标志,并根据其当前值应用 属性 设置器。
最后,我要指出,对于像这样在 UI 中重复更改状态,另一种方法是使用 WPF 的动画功能。这超出了这个答案的范围,但我鼓励您阅读它。这样做的一个好处是动画使用比我上面使用的基于线程池的 Task.Delay()
方法更高分辨率的计时模型,因此通常会提供更平滑的动画(尽管,当然随着你的间隔得到越来越小——比如你的 post 表示你打算使用的 25 毫秒——无论如何你都很难让 WPF 平稳地跟上;在某些时候,你会发现更高级别的 UI WinForms、WPF、Xamarin 等框架无法在如此细粒度的计时器级别上运行。
我在 VS2017 中创建了一个新的 WPF 项目,还通过 NuGet 导入了 MVVM Light。
然后我添加了一些代码,每 25 毫秒从 MainWindows Grid 更改背景颜色。可悲的是,这种变化没有传播,我不知道为什么它没有更新。也许这里有人可以帮助我。
代码如下:
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只是定期生成的文件)
<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>
格雷格
您的代码中的主要问题是 System.Windows.Media.SolidColorBrush
没有覆盖 Equals()
方法,因此您的表达式 Background == Brushes.Black
永远不会是 true
。由于您正在创建 SolidColorBrush
对象的显式新实例,并且由于 ==
运算符只是比较实例引用,因此您的画笔值与内置 Brushes.Black
实例之间的比较总是失败。
修复代码的最简单方法是只使用实际的 Brushes
个实例:
private void Timer_Tick(object sender, System.EventArgs e)
{
if (Background == Brushes.Black)
{
Background = Brushes.White;
Title = "White";
}
else
{
Background = Brushes.Black;
Title = "Black";
}
}
那么当你比较实例引用时,它们实际上是可比较的,你会根据需要检测"black"条件。
我会注意到,由于您也没有为 Title
属性 的更改提出 PropertyChanged
,因此绑定也不会按预期工作。
无论如何,我会完全避免使用您的设计。首先,视图模型对象应避免使用 UI 特定类型。当然,这将包括 Brush
类型。可以说,它还包括 DispatcherTimer
,因为它存在于 UI 的服务中。除此之外,DispatcherTimer
是一个相对不精确的计时器,虽然它的存在主要是为了让一个计时器在拥有该计时器的调度程序线程上引发其 Tick
事件,因为 WPF 会自动封送 属性 - 将事件从任何其他线程更改为 UI 线程,在本例中几乎没有用处。
恕我直言,这是一个更符合典型 WPF 编程实践的程序版本:
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;
}
}
}
此视图模型 class 使用我用于所有视图模型的基础 class,因此我不必一直重新实现 INotifyPropertyChanged
:
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));
}
}
您会注意到视图模型 class 没有任何 UI 特定的行为。它适用于任何程序,WPF 或其他程序,只要该程序能够对 PropertyChanged
事件作出反应,并利用视图模型中的值。
为了完成这项工作,XAML 变得更加冗长:
<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>
(注意:我已经明确命名了 http://schemas.microsoft.com/netfx/2007/xaml/presentation XML 命名空间,用于 <Style/>
元素,仅作为 Stack Overflow 不足 XML 标记处理,否则不会将 <Style/>
元素识别为实际的 XML 元素。在您自己的程序中,您可以随意将其省略。)
这里的关键是 UI 关注点的整个处理都在 UI 声明本身。视图模型不需要知道 UI 如何表示黑色或白色。它只是切换一个标志。然后 UI 监视该标志,并根据其当前值应用 属性 设置器。
最后,我要指出,对于像这样在 UI 中重复更改状态,另一种方法是使用 WPF 的动画功能。这超出了这个答案的范围,但我鼓励您阅读它。这样做的一个好处是动画使用比我上面使用的基于线程池的 Task.Delay()
方法更高分辨率的计时模型,因此通常会提供更平滑的动画(尽管,当然随着你的间隔得到越来越小——比如你的 post 表示你打算使用的 25 毫秒——无论如何你都很难让 WPF 平稳地跟上;在某些时候,你会发现更高级别的 UI WinForms、WPF、Xamarin 等框架无法在如此细粒度的计时器级别上运行。