Как обновить текстовый блок в режиме реального времени? - PullRequest
0 голосов
/ 10 июля 2019

Я работаю над проектом с фреймворком MVVM Light.
У меня есть MainViewModel, которая помогает мне перемещаться между моделями представления. У меня есть методы GoBack и GoTo. Их меняют CurrentViewModel.

private RelayCommand<string> _goTo;
public RelayCommand<string> GoTo
        {
            get
            {
                return _goTo
                    ?? (_goTo = new RelayCommand<string>(view
                     =>
                    {
                        SwitchView(view);
                    }));
            }
        }
 private void SwitchView(string name)
        {
            switch (name)
            {
                case "login":
                    User = null;
                    CurrentViewModel = new LoginViewModel();
                    break;
                case "menu":
                    CurrentViewModel = new MenuViewModel();
                    break;
                case "order":
                    CurrentViewModel = new OrderViewModel();
                    break;
            }

В MainWindow есть управление контентом и шаблоны данных.

[...]
 <DataTemplate DataType="{x:Type vm:LoginViewModel}">
            <view:Login/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:MenuViewModel}">
            <view:Menu/>
        </DataTemplate>
[...]

<ContentControl VerticalAlignment="Top" HorizontalAlignment="Stretch" 
                            Content="{Binding CurrentViewModel}" IsTabStop="false"/>

В моем OrderView (это UserControl) у меня есть текстовый блок, который должен отображать TotalPrice заказа.

<TextBlock Text="{Binding AddOrderView.TotalPrice}"  Padding="0 2 0 0" FontSize="20" FontWeight="Bold" HorizontalAlignment="Right"/>

OrderViewModel имеет свойство TotalPrice, и оно работает хорошо. Когда я отлаживаю, я вижу, что это изменилось, но в моем представлении ничего не произошло.

        private decimal _totalPrice;
        public decimal TotalPrice
        {
            get
            {
                _totalPrice = 0;
                foreach (var item in Products)
                {
                    item.total_price = item.amount * item.price;
                    _totalPrice += item.price * item.amount;
                }
                return _totalPrice;
            }
            set
            {
                if (_totalPrice == value)
                    return;
                _totalPrice = value;
                RaisePropertyChanged("TotalPrice");
            }
        }

OrderViewModel наследует от BaseViewModel и реализует INotifyPropertyChanged.

Почему мой текстовый блок не обновляется / не обновляется? Как это сделать?

Когда я меняю вид кнопкой назад и снова захожу в OrderView, я вижу изменения!

Я провожу несколько дней в поисках решения, и ничто не помогает мне.

https://i.stack.imgur.com/K8lip.gif

Так что, похоже, когда View устанавливает, нет способа изменить его без перезагрузки. Я не знаю, как именно это работает.

Ответы [ 2 ]

0 голосов
/ 10 июля 2019

Вы не должны выполнять вычисления или любые трудоемкие операции в получателе или установщике свойства.Это может резко ухудшить производительность.Если вычисления или операция отнимают много времени, вы должны выполнить их в фоновом потоке и вызвать событие PropertyChanged после завершения Task.Таким образом, вызов метода получения или установки свойства не замораживает пользовательский интерфейс.

Объяснение наблюдаемого вами поведения:
Побочный эффект изменения значения свойства в своем собственном getter вместо установщика заключается в том, что новое значение не будет распространяться на цели привязки.Получатель вызывается только привязкой, когда произошло событие PropertyChanged.Таким образом, выполнение вычислений в геттере не приводит к обновлению привязки.Теперь при перезагрузке страницы все привязки будут инициализировать цель привязки и, следовательно, вызывать метод получения свойства.

Необходимо установить свойство TotalPrice (а не поле поддержки), чтобы инициировать обновление цели привязки.Но, как вы уже испытали, повышение значения PropertyChanged свойства в том же геттере приведет к бесконечному циклу и, следовательно, к StackOverflowException.
. Кроме того, вычисление всегда будет выполняться при каждом обращении к геттеру свойства.- даже если TotalPrice не изменилось.

Значение TotalPrice зависит от свойства Products.Чтобы свести к минимуму вероятность вычисления TotalPrice, рассчитывайте только при изменении Products:

OrderViewModel.cs

public class OrderViewModel : ViewModelBase
{
  private decimal _totalPrice;
  public decimal TotalPrice
  {
    get => this._totalPrice;
    set
    {
      if (this._totalPrice == value)
        return;
      this._totalPrice = value;

      RaisePropertyChanged();
    }
  }

  private ObservableCollection<Product> _products;
  public ObservableCollection<Product> Products
  {
    get => this._products;
    set
    {    
      if (this.Products == value)
        return;

      if (this.Products != null)
      {
        this.Products.CollectionChanged -= OnCollectionChanged;
        UnsubscribeFromItemsPropertyChanged(this.Products);
      }

      this._products = value;

      this.Products.CollectionChanged += OnCollectionChanged;
      if (this.Products.Any())
      {
        SubscribeToItemsPropertyChanged(this.Products);
      }

      RaisePropertyChanged();
    }
  }

  private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  {
    if (!e.Action.Equals(NotifyCollectionChangedAction.Move))
    {
      UnsubscribeFromItemsPropertyChanged(e.OldItems);
      SubscribeToItemsPropertyChanged(e.NewItems);
    }

    CalculateTotalPrice();
  }

  private void ProductChanged(object sender, PropertyChangedEventArgs e) => CalculateTotalPrice();

  private void SubscribeToItemsPropertyChanged(IList newItems) => newItems?.OfType<INotifyPropertyChanged>().ToList().ForEach((item => item.PropertyChanged += ProductChanged));

  private void UnsubscribeFromItemsPropertyChanged(IEnumerable oldItems) => oldItems?.OfType<INotifyPropertyChanged>().ToList().ForEach((item => item.PropertyChanged -= ProductChanged));

  private void CalculateTotalPrice() => this.TotalPrice = this.Products.Sum(item => item.total_price);

  private void GetProducts()
  {
    using (var context = new mainEntities())
    {
      var result = context.product.Include(c => c.brand);
      this.Products = new ObservableCollection<Product>(
        result.Select(item => new Product(item.name, item.mass, item.ean, item.brand.name, item.price)));
    }
  }

  public void ResetOrder()
  {
    this.Products
      .ToList()
      .ForEach(product => product.Reset());
    this.TotalPrice = 0;
  }

  public OrderViewModel()
  {
    SetView("Dodaj zamówienie");
    GetProducts();
  }
}

Также убедитесь, что Product (предметы из коллекции Products) тоже реализуют INotifyPropertyChanged.Это обеспечит возникновение события Products.CollectionChanged при изменении свойств Product.

Чтобы исправить поведение переключения страниц, необходимо изменить класс MainViewModel:

MainViewModel.cs

public class MainViewModel : ViewModelBase
{
  // The page viewmodels  
  private Dictionary<string, ViewModelBase> PageViewModels { get; set; }
  public Stack<string> ViewsQueue;

  public MainViewModel()
  {
    User = new User(1, "login", "name", "surname", 1, 1, 1);

    this.PageViewModels = new Dictionary<string, ViewModelBase>()
    {
      {"login", new LoginViewModel()},
      {"menu", new MenuViewModel()},
      {"order", new OrderViewModel()},
      {"clients", new ClientsViewModel(User)}
    };

    this.CurrentViewModel = this.PageViewModels["login"];

    this.ViewsQueue = new Stack<string>();
    this.ViewsQueue.Push("login");

    Messenger.Default.Register<NavigateTo>(
      this,
      (message) =>
      {
        try
        {
          ViewsQueue.Push(message.Name);
          if (message.user != null) User = message.user;
          SwitchView(message.Name);
        }
        catch (System.InvalidOperationException e)
        {
        }
      });

    Messenger.Default.Register<GoBack>(
      this,
      (message) =>
      {
        try
        {
          ViewsQueue.Pop();
          SwitchView(ViewsQueue.Peek());
        }
        catch (System.InvalidOperationException e)
        {
        }
      });
  }

  public RelayCommand<string> GoTo => new RelayCommand<string>(
    viewName =>
    {
      ViewsQueue.Push(viewName);
      SwitchView(viewName);
    });


  protected void SwitchView(string name)
  {
    if (this.PageViewModels.TryGetValue(name, out ViewModelBase nextPageViewModel))
    {
      if (nextPageViewModel is OrderViewModel orderViewModel)
        orderViewModel.ResetOrder();

      this.CurrentViewModel = nextPageViewModel;
    }
  }
}

Ваш измененный Product.cs

public class Product : ViewModelBase
{
  public long id { get; set; }
  public string name { get; set; }
  public decimal mass { get; set; }
  public long ean { get; set; }
  public long brand_id { get; set; }
  public string img_source { get; set; }
  public string brand_name { get; set; }

  private decimal _price;
  public decimal price
  {
    get => this._price;
    set
    {
      if (this._price == value)
        return;

      this._price = value;
      OnPriceChanged();
      RaisePropertyChanged();
    }
  }

  private long _amount;
  public long amount
  {
    get => this._amount;
    set
    {
      if (this._amount == value)
        return;

      this._amount = value;
      OnAmountChanged();
      RaisePropertyChanged();
    }
  }

  private decimal _total_price;
  public decimal total_price
  {
    get => this._total_price;
    set
    {
      if (this._total_price == value)
        return;

      this._total_price = value;
      RaisePropertyChanged();
    }
  }

  public Product(long id, string name, decimal mass, long ean, long brandId, decimal price, string imgSource)
  {
    this.id = id;
    this.name = name;
    this.mass = mass;
    this.ean = ean;
    this.brand_id = brandId;
    this.price = price;
    this.img_source = imgSource;
  }

  public Product(string name, decimal mass, long ean, string brandName, decimal price)
  {
    this.id = this.id;
    this.name = name;
    this.mass = mass;
    this.ean = ean;
    this.brand_name = brandName;
    this.price = price;
  }

  public void Reset()
  {
    // Resetting the `amount` will trigger recalculation of `total_price`
    this.amount = 0;
  }

  protected virtual void OnAmountChanged()
  {
    CalculateTotalPrice();
  }

  private void OnPriceChanged()
  {
    CalculateTotalPrice();
  }

  private void CalculateTotalPrice()
  {
    this.total_price = this.price * this.amount;
  }
}

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

0 голосов
/ 10 июля 2019

Это не обновление, потому что вы только звоните RaisePropertyChanged("TotalPrice"); в сеттер.В то время как в вашем геттере находится расчет.Поэтому в любое время, когда вы изменяете свойство Products или содержимое коллекции Products, вам также необходимо вызывать RaisePropertyChanged("TotalPrice");, чтобы уведомить View о том, что TotalPrice обновлено.

Так что, если вы измените какой-либо элемент item.amount или item.price или добавите или удалите элементы из списка продуктов, вам также необходимо позвонить.RaisePropertyChanged("TotalPrice");

Пример:

Products.Add(item);
RaisePropertyChanged("TotalPrice"); //This will tell you're View to check for the new value from TotalPrice
...