Привязка цвета фона не обновляется из таймера диспетчера - PullRequest
1 голос
/ 31 марта 2019

Я создал новый проект WPF в VS2017, а также импортировал MVVM Light через NuGet.

Затем я добавил код, который должен менять цвет фона из сетки MainWindows каждые 25 миллисекунд. К сожалению, это изменение не распространяется, и я понятия не имею, почему оно не обновляется. Может быть, кто-то здесь может помочь мне.

Вот код:

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>

Greg

1 Ответ

3 голосов
/ 31 марта 2019

Основная проблема в вашем коде заключается в том, что 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";
    }
}

Затем, когда вы сравниваете ссылки на экземпляры, они на самом деле сопоставимы, и вы обнаружите "черное "условие по желанию.

Я отмечу, что, поскольку вы также не повышаете PropertyChanged для изменений свойства Title, эта привязка также не будет работать должным образом.

Для чего бы это ни стоило, я бы вообще избежал вашего дизайна.Во-первых, при просмотре объектов модели следует избегать использования специфичных для пользовательского интерфейса типов.Конечно, это будет включать тип Brush.Возможно, он также включает в себя DispatcherTimer, так как он существует в обслуживании пользовательского интерфейса.Помимо этого, DispatcherTimer является относительно неточным таймером, и хотя он существует, главным образом, для того, чтобы иметь таймер, который вызывает его событие Tick в потоке диспетчера, которому принадлежит таймер, поскольку WPF автоматически маршализирует события изменения свойств из любого другого потока впоток пользовательского интерфейса, он не так полезен в этом примере.

Вот версия вашей программы, которая IMHO больше соответствует типичным практикам программирования 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;
        }
    }
}

Эта модель представлениякласс использует базовый класс, который я использую для всех своих моделей представлений, поэтому мне не нужно постоянно переопределять 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));
    }
}

Вы заметите, что класс модели представлений неиметь любое поведение, специфичное для пользовательского интерфейса.Он будет работать с любой программой, 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/>, исключительно как обходной путь для стекаНедостаточная обработка XML-разметки в Overflow, которая иначе не распознала бы элемент <Style/> в качестве фактического элемента XML. В вашей собственной программе вы можете не обращать на это внимания.)

Ключом здесь является то, что весьобработка озабоченности UI находится в самой декларации UI.Модель представления не должна знать, как пользовательский интерфейс представляет черный или белый цвета.Это просто переключает флаг.Затем пользовательский интерфейс отслеживает этот флаг и применяет установщики свойств в соответствии с его текущим значением.

Наконец, я отмечу, что для многократного изменения состояния в пользовательском интерфейсе, подобного этому, другой подход заключается в использовании функций анимации WPF.,Это выходит за рамки этого ответа, но я призываю вас прочитать об этом.Одним из преимуществ этого является то, что для анимации используется модель синхронизации даже с более высоким разрешением, чем метод Task.Delay(), основанный на пуле потоков, который я использовал выше, и, как правило, она обеспечивает еще более плавную анимацию (хотя, конечно, поскольку ваш интервал становится меньше именьше - например, 25 мс, поскольку ваше сообщение указывает на то, что вы намеревались использовать - у вас будут проблемы с тем, чтобы WPF не отставал, независимо от того, в какой-то момент вы обнаружите, что высокоуровневые инфраструктуры пользовательского интерфейса, такие как WinForms, WPF, Xamarin и т. д.просто не могу работать на таком мелкозернистом уровне таймера).

...