首页 > 解决方案 > 编辑属性后不刷新来自相关实体的转换值

问题描述

我在 VS2019 中使用 .net core 3.1。我有一个用户控件,其中包含一个 ListView,其中列出了价目表的一组项目,每个项目都有一个成本、标记(百分比)、一个名为 Promotion 的相关实体给出的折扣(百分比)以及SalePrice、FinalPrice 和 PromoDescription字段由转换器计算。例如,如果商品没有促销, FinalPrice等于 SalePrice,否则为 (SalePrice - SalePrice*item.Promotion.Discount/100.0)。

顺便说一句,所有这些实体都属于带有代理延迟加载的 EF Core 模型,并且所有集合都是 ObervableCollection,它们的加载方式如下:

var dbset = Context.Promos;
            dbset.Load();
            return dbset.Local.ToObservableCollection();

一切正常,除非我更改标记或促销属性,这会导致重新计算转换器提供的值并且不涉及或涉及 Promo 类型的相关实体。调试窗口我可以看到在确认到数据库之前和之后在底层视图模型中正确应用的所有更改,但是我必须强制它刷新值,如下图所示。

在此动画中,问题得到了很好的体现: 1) 进入编辑模式开始编辑所选项目。 2)更改促销相关实体(

这是窗口的最终组成:

这是模型:

英孚模型

这是包含列表视图的用户控件的 wpf:

<UserControl x:Class="MyApp.View.UserControls.PriceListUserControl"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:MyApp.View.UserControls"
         xmlns:localModel="clr-namespace:MyApp.Model"
         xmlns:localVm="clr-namespace:MyApp.ViewModel"
         mc:Ignorable="d" 
         d:DesignHeight="450" d:DesignWidth="800" 
         x:Name="UserControlPriceList">
<UserControl.Resources>
    <localVm:PriceListItemConverter x:Key="priceListItemConverter"/>
    <localVm:DateConverter x:Key="dateConverter"/>
    <localVm:PromoConverter x:Key="promoConverter" />

        <Border Style="{StaticResource BorderStyle}" Name="bottomBorder">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="106" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <TextBlock Grid.Row="0" Grid.Column="0" Style="{StaticResource SmallTitleStyle}" Margin="5"> Producto:</TextBlock>
                <TextBlock Grid.Row="1" Grid.Column="0" Style="{StaticResource SmallTitleStyle}" Margin="5"> Costo:</TextBlock>
                <TextBlock Grid.Row="2" Grid.Column="0" Style="{StaticResource SmallTitleStyle}" Margin="5"> Markup (%):</TextBlock>
                <TextBlock Grid.Row="3" Grid.Column="0" Style="{StaticResource SmallTitleStyle}" Margin="5"> Precio de Lista:</TextBlock>
                <TextBlock Grid.Row="4" Grid.Column="0" Style="{StaticResource SmallTitleStyle}" Margin="5"> Promoción:</TextBlock>
                <TextBlock Grid.Row="5" Grid.Column="0" Style="{StaticResource SmallTitleStyle}" Margin="5"> Precio Final:</TextBlock>

                <TextBlock Name="ProductDescription" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left"
                         Text="{Binding Path=Product.Description}"
                         Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
                <TextBlock Name="CostPrice" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left"
                         Text="{Binding Path=Product.CostPrice, StringFormat=C}"
                         Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
                <TextBox Name="MarkupPriceEntryForm" AutomationProperties.Name="Markup" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left"
                            Validation.ErrorTemplate="{StaticResource ValidationTemplate}"
                         Style="{StaticResource TextStyleTextBox}" Margin="8,5,0,5" IsReadOnly="{Binding ElementName=UserControlPriceList, Path=DataContext.IsReadOnly}" >
                    <TextBox.Text>
                        <Binding Path="Markup" StringFormat="N2" UpdateSourceTrigger="PropertyChanged">
                            <Binding.ValidationRules>
                                <ExceptionValidationRule />
                            </Binding.ValidationRules>
                        </Binding>
                    </TextBox.Text>
                </TextBox>
                <TextBlock Name="SalePrice" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Left"
                         Text="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=SalePrice, StringFormat=C}"
                         Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
                <TextBlock Name="Promotion" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Left" 
                       Text="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=PromoFriendlyDescription}"
                         Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" 
                         Visibility="{Binding ElementName=UserControlPriceList, Path=DataContext.HideIfEditing}"/>
                <ComboBox Name="PromosComboBox" AutomationProperties.Name="PromoDiscount" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Left"                             
                          Text="(Seleccione promo)"
                          ItemsSource="{Binding ElementName=UserControlPriceList, Path=DataContext.PromoCollection}"                              
                          SelectedValue="{Binding ElementName=UserControlPriceList, Path=DataContext.Current.PromoId, Mode=TwoWay}"
                          SelectedValuePath="Id"                               
                          Style="{StaticResource ComboBoxStyle}"
                          ItemContainerStyle="{StaticResource ComboBoxItemStyle}" Margin="8,5,0,5"
                          IsTextSearchEnabled="True"
                          IsTextSearchCaseSensitive="False"
                          IsDropDownOpen="False"
                          StaysOpenOnEdit="True"                              
                          IsEditable="True" IsReadOnly="False"
                          Visibility="{Binding ElementName=UserControlPriceList, Path=DataContext.ShowIfEditing}" 
                          Width="{Binding ElementName=Promotion, Path=Width}">
                    <ComboBox.Resources>
                        <DataTemplate DataType="{x:Type localModel:Promo}">
                            <TextBlock Text="{Binding Converter={StaticResource promoConverter}, ConverterParameter=PromoFriendlyDescription}"/>
                        </DataTemplate>
                    </ComboBox.Resources>                                               
                </ComboBox>                    
                <TextBlock Name="FinalPrice" Grid.Row="5" Grid.Column="1" HorizontalAlignment="Left"
                         Text="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=FinalPrice, StringFormat=C}"
                         Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
            </Grid>
        </Border>
    </DataTemplate>
</UserControl.Resources>

<Border Padding="20">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Style="{StaticResource MediumTitleStyle}" Margin="5" 
                   Text="{Binding Path=List.Name, StringFormat='Lista: {0}'}" />
        <TextBlock Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2" Style="{StaticResource MediumTitleStyle}" Margin="5" HorizontalAlignment="Right"
                   Text="{Binding Path=List.PrintDate, StringFormat='Última impresión: {0:dd/MM/yyyy}'}" />

        <Border Grid.Row="1" Style="{StaticResource BorderStyle}" Grid.ColumnSpan="4">
            <ListView Name="ListViewPriceListItems" Grid.Row="1" Grid.ColumnSpan="4" 
                          ItemsSource="{Binding Path=Items}" 
                      IsSynchronizedWithCurrentItem="True"
                      SelectionChanged="OnPriceListItemSelectionChanged"
                      MouseDoubleClick="EnterPriceListItemEditMode"
                      IsEnabled="{Binding Path=IsReadOnly}">
                <ListView.View>
                    <GridView AllowsColumnReorder="true" ColumnHeaderToolTip="Detalle de productos, precios finales y promociones">
                        <GridViewColumn DisplayMemberBinding="{Binding Path=Product.Description}" Header="Producto" Width="300"/>
                        <GridViewColumn DisplayMemberBinding="{Binding Path=Product.CostPrice, StringFormat=C}" Header="Costo"/>
                        <GridViewColumn DisplayMemberBinding="{Binding Path=Markup, StringFormat=N2}" Header="Markup (%)" />
                        <GridViewColumn DisplayMemberBinding="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=SalePrice, StringFormat=C}" Header="Precio Lista"/>
                        <GridViewColumn DisplayMemberBinding="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=PromoFriendlyDescription}" Header="Promoción" Width="200" />
                        <GridViewColumn DisplayMemberBinding="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=FinalPrice, StringFormat=C}" Header="Precio Final" />
                    </GridView>
                </ListView.View>
            </ListView>
        </Border>
        <ContentControl Name="PriceListDetail" Grid.Row="2" Grid.ColumnSpan="4"
                    Content="{Binding Path=Current}"
                    ContentTemplate="{StaticResource PriceListItemDetailTemplate}"
                    Margin="9,0,0,0" />

        <StackPanel Grid.Row="3" Grid.ColumnSpan="4" Orientation="Horizontal" HorizontalAlignment="Right">
            <StackPanel.Resources>
                <Style TargetType="{x:Type Button}">
                    <Setter Property="Margin" Value="0,0,0,0"/>
                </Style>
            </StackPanel.Resources>
            <Button Name="EditButton" HorizontalAlignment="Right" Content="_Modificar" Style="{StaticResource AcctionButtonStyle}" 
                        ToolTip="Editar precio y promoción del producto seleccionado en esta lista."
                        Click="EditSelectedPriceListItem" Visibility="{Binding Path=HideIfEditing}"/>
            <Button Name="SaveButton" HorizontalAlignment="Right" Content="_Aceptar" Style="{StaticResource ActionButtonOKStyle}" 
                        ToolTip="Guardar los cambios realizados."
                        Click="SavePriceListItem" Visibility="{Binding Path=ShowIfEditing}"/>
            <Button Name="CancelButton" HorizontalAlignment="Right" Content="_Cancelar" Style="{StaticResource ActionButtonCancelStyle}" 
                        ToolTip="Deshacer los cambios realizados."
                        Click="UndoPriceListItem" Visibility="{Binding Path=ShowIfEditing}"/>
        </StackPanel>
    </Grid>
</Border>

这是它的视图模型:

public partial class PriceListItemsViewModel: BaseViewModel<PriceListItem>
{
    private PriceList _list;

    public PriceListItemsViewModel(PriceList list, ObservableCollection<Promo> promoCollection)
    {

        this.List = list ?? throw new ArgumentNullException(nameof(list));
        this.PromoCollection = promoCollection;
        this.Items = list.Products;         
        this.IsEditMode = false;
        this.IsNew = false;
        this.Current = this.Items.FirstOrDefault();
    }

    public PriceList List { get => this._list; set => this._list = value; }
    public ObservableCollection<Promo> PromoCollection { get; }
}

注意:BaseViewModel 有一个 ObservableCollection,实现了 Generic INotifyPropertyChange,以及用于指示该集合中的当前选定项和其他有用状态的通用属性

这里是包含先前用户控件的视图的 wpf:

<Window.Resources>                
    <uc:PriceListUserControl x:Key="userControlPList"/>
</Window.Resources>

<Border Padding="20">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TabControl x:Name="TabControlPriceLists" Grid.Row="1" Grid.ColumnSpan="3" TabStripPlacement="Top"
                    ItemsSource="{Binding Items}"
                    SelectedItem="{Binding Current}"
                    IsEnabled="{Binding IsReadOnly}" >
            <TabControl.Resources>
                <DataTemplate DataType="{x:Type localVm:PriceListItemsViewModel}">
                    <uc:PriceListUserControl x:Name="UserControlPriceList"/>
                </DataTemplate>
            </TabControl.Resources>
            <TabControl.ItemContainerStyle>
                <Style TargetType="TabItem">
                    <Setter Property="Header" Value="{Binding List.Name}" />
                    <Setter Property="MaxWidth" Value="100" />
                    <Setter Property="ToolTip" Value="{Binding List.Name}" />
                    <Setter Property="Background" Value="Bisque" />
                    <Setter Property="FontWeight" Value="DemiBold" />                        
                    <EventSetter Event="MouseDoubleClick" Handler="OnTabItemDoubleClick"/>
                </Style>                    
            </TabControl.ItemContainerStyle>
        </TabControl>

    </Grid>
</Border>

及其视图模型:

public class PriceListsViewModel : BaseViewModel<PriceListItemsViewModel>
{
    private ObservableCollection<Promo> promoCollection;
    public PriceListsViewModel()
    {
        var promoDefaultItems = new List<Promo>
        {
            new Promo { FriendlyName = PromoConverter.PromoFriendlyNameNoPromo, DiscountPct = 0.0F } //Workaround: Adds "(No promo) to allow the user to left an item without promotion
            //new Promo { FriendlyName = PromoConverter.PromoFriendlyNameNewPromo, DiscountPct = 0F }
        };
        this.promoCollection = new ObservableCollection<Promo>(promoDefaultItems);
        var promos = PromoRepository.PromoGetAll();
        foreach (var promo in promos)
        {
            this.promoCollection.Add(promo);
        }
        var lists = PriceListRepository.PriceListGetAll();
        this.Items = new ObservableCollection<PriceListItemsViewModel>();
        foreach (var l in lists)
        {
            this.Items.Add(new PriceListItemsViewModel(l, promoCollection));
        }
        this.IsEditMode = false;
        this.IsNew = false;
        this.Current = this.Items.FirstOrDefault();

    }
}

标签: wpfentity-frameworklistviewdata-bindingivalueconverter

解决方案


我通过创建一个新的视图模型来解决这个问题,该模型包含以前的 PriceListItem 并提供真实属性并通知它们的更改。这是新的视图模型(仅显示新属性的相关代码)

public class PriceListItemViewModel : SupervisedEntity
{
    private PriceListItem _item;        

    public PriceListItemViewModel(PriceListItem item)
    {
        this._item = item ?? throw new ArgumentNullException(nameof(item));         

        //Bind the parent properties to its dependants (actually they are computed properties without setter) so they notify property changes when their parent property changes.
        this.AddDependentProperty(nameof(this.PromoId), nameof(this.SalePrice));
        this.AddDependentProperty(nameof(this.PromoId), nameof(this.FinalPrice));
        this.AddDependentProperty(nameof(this.PromoId), nameof(this.PromoFriendlyDescription));
        this.AddDependentProperty(nameof(this.Markup), nameof(this.SalePrice));
        this.AddDependentProperty(nameof(this.Markup), nameof(this.FinalPrice));            
    }

    // The properties and code not related to the solution were removed for clarity

    public int? PromoId
    {
        get => this._item.PromoId;
        set
        {
            this._item.PromoId = (value is null || value == 0) ? null : value;              
            this.NotifyPropertyChanged(); //This notifies not only the change in PromoId but also in all its dependent properties
        }
    }

    public string ProductDescription => this._item.Product.Description;

    public float CostPrice => this._item.Product.CostPrice;

    [Required]
    public float Markup
    {
        get => this._item.Markup;
        set
        {
            this._item.Markup = value;
            this.NotifyPropertyChanged();
        }
    }

    //Using properties instead of extension methods allows me to create notification for them as dependent
    public float SalePrice => this._item.SalePrice(); 

    public float FinalPrice => this._item.FinalPrice();

    public int Id => this.Item.Id;

    public string PromoFriendlyDescription => this._item.PromoId is null ? string.Empty : this._item.Promotion.ToString();      
}

在用户控件的 xaml 中,我用新视图模型中的实际属性替换了所有转换器,当它们依赖的属性之一发生更改时,它们没有得到通知。首先,用新的视图模型替换模型的类,如下所示:

<DataTemplate x:Key="PriceListItemDetailTemplate" DataType="{x:Type localVm:PriceListItemViewModel}">

然后替换更改的属性中对转换器的所有引用:

<TextBlock Name="SalePrice" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Left"
                     Text="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=SalePrice, StringFormat=C}"
                     Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
<TextBlock Name="FinalPrice" Grid.Row="5" Grid.Column="1" HorizontalAlignment="Left"
                     Text="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=FinalPrice, StringFormat=C}"
                     Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />

通过新视图模型中的真实属性:

<TextBlock Name="SalePrice" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Left"
                         Text="{Binding Path=SalePrice, StringFormat=C}"
                         Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />                      
<TextBlock Name="FinalPrice" Grid.Row="5" Grid.Column="1" HorizontalAlignment="Left"
                         Text="{Binding Path=FinalPrice, StringFormat=C}"
                         Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />

现在它可以工作了。


推荐阅读