如何实时更新文本块?
How to update textblock in real-time?
我正在使用 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)中,我有一个文本块,它应该显示订单的总价格。
<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。
为什么我的文本块不是 updating/refreshing?如何做到这一点?
当我使用后退按钮更改视图并再次转到 OrderView 时,我看到了变化!
我花了几天时间寻找解决方案,但没有任何帮助。
https://i.stack.imgur.com/K8lip.gif
所以看起来当 View 正在设置时,如果不重新加载它就无法更改它。我不知道它是如何工作的。
它没有更新,因为您只在 setter 中调用 RaisePropertyChanged("TotalPrice");
。而在您的 getter 中是计算。所以任何时候你改变 Products
属性 或者 Products
集合的内容,你还需要调用 RaisePropertyChanged("TotalPrice");
通知视图 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
您不应在 属性 的 getter 或 setter 中进行计算或任何耗时的操作。这会大大降低性能。如果计算或操作很耗时,那么您应该在后台线程中执行它,并在 Task
完成后引发 PropertyChanged
事件。这样调用 属性 的 getter 或 setter 就不会冻结 UI.
对您观察到的行为的解释:
在 getter 而不是 setter 中更改 属性 值的副作用是新值不会传播到绑定目标。 getter 仅在 PropertyChanged
事件发生时由绑定调用。所以在 getter 中进行计算不会触发绑定刷新。现在,当重新加载页面时,所有绑定都将初始化绑定目标并因此调用 属性 getter。
您必须设置 TotalPrice
属性(而不是支持字段)才能触发绑定目标的刷新。但是正如您自己已经经历过的那样,在 属性 中引发 PropertyChanged
事件
相同的 getter 将导致无限循环,因此导致 WhosebugException
.
此外,每当访问 属性 的 getter 时,计算将始终执行 - 即使 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
。这将确保在 Product
属性更改时引发 Products.CollectionChanged
事件。
要修复页面切换行为,您必须修改 MainViewModel
class:
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;
}
}
问题是您在切换到页面时总是创建一个新的视图模型。当然,所有以前的页面信息都会丢失。您必须重用相同的视图模型实例。为此,只需将它们存储在您在构造函数中初始化一次的专用私有 属性 中。
我正在使用 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)中,我有一个文本块,它应该显示订单的总价格。
<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。
为什么我的文本块不是 updating/refreshing?如何做到这一点?
当我使用后退按钮更改视图并再次转到 OrderView 时,我看到了变化!
我花了几天时间寻找解决方案,但没有任何帮助。
https://i.stack.imgur.com/K8lip.gif
所以看起来当 View 正在设置时,如果不重新加载它就无法更改它。我不知道它是如何工作的。
它没有更新,因为您只在 setter 中调用 RaisePropertyChanged("TotalPrice");
。而在您的 getter 中是计算。所以任何时候你改变 Products
属性 或者 Products
集合的内容,你还需要调用 RaisePropertyChanged("TotalPrice");
通知视图 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
您不应在 属性 的 getter 或 setter 中进行计算或任何耗时的操作。这会大大降低性能。如果计算或操作很耗时,那么您应该在后台线程中执行它,并在 Task
完成后引发 PropertyChanged
事件。这样调用 属性 的 getter 或 setter 就不会冻结 UI.
对您观察到的行为的解释:
在 getter 而不是 setter 中更改 属性 值的副作用是新值不会传播到绑定目标。 getter 仅在 PropertyChanged
事件发生时由绑定调用。所以在 getter 中进行计算不会触发绑定刷新。现在,当重新加载页面时,所有绑定都将初始化绑定目标并因此调用 属性 getter。
您必须设置 TotalPrice
属性(而不是支持字段)才能触发绑定目标的刷新。但是正如您自己已经经历过的那样,在 属性 中引发 PropertyChanged
事件
相同的 getter 将导致无限循环,因此导致 WhosebugException
.
此外,每当访问 属性 的 getter 时,计算将始终执行 - 即使 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
。这将确保在 Product
属性更改时引发 Products.CollectionChanged
事件。
要修复页面切换行为,您必须修改 MainViewModel
class:
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;
}
}
问题是您在切换到页面时总是创建一个新的视图模型。当然,所有以前的页面信息都会丢失。您必须重用相同的视图模型实例。为此,只需将它们存储在您在构造函数中初始化一次的专用私有 属性 中。