Отлов события изменения ObservableCollection

178
26 апреля 2018, 07:57

Есть приложение в котором пользователь может указывать название продукта и его цену, по нажатию на кнопку эти данные вносятся в ObservableCollection и сразу же отображаются в DataGrid. Также в окне присутствует textbox в котором должна отображаться средняя цена всех внесенных продуктов. Проблема в следующем: Нужно чтобы пользователь в DataGrid мог изменять цену уже внесённых продуктов после чего сразу же должна изменяться средняя цена (AvaragePrice) всех продуктов, а этого не происходит, данные в коллекции изменяются, но изменения вышеуказанного свойства не происходит: MainWindow.xaml:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="300"/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <DataGrid Background="Transparent" ItemsSource="{Binding Path=AllProducts, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged,
              NotifyOnSourceUpdated=True}" 
              AutoGenerateColumns="False" HorizontalAlignment="Center" 
              FontSize="16" Foreground="Black" CanUserReorderColumns="True">
        <DataGrid.Columns>
            <DataGridTextColumn Header="Name" Binding="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
            <DataGridTextColumn Header="Price" Binding="{Binding Path=Price, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        </DataGrid.Columns>
    </DataGrid>
    <Grid Grid.Column="1">
        <!--Разбиваем наш грид на строки-->
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Label Content="Name of product:" FontSize="16" 
                   Foreground="White" VerticalAlignment="Center"/>
            <Label Grid.Row="1" Content="Price (zl):" 
                   FontSize="16" Foreground="White" 
                   VerticalAlignment="Center"/>
            <TextBox Grid.Column="1" Background="Transparent" 
                     Height="35" Foreground="White" 
                     FontSize="18" Width="200" CaretBrush="White"                    
                     HorizontalAlignment="Left" Text="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged}"/>
            <TextBox Grid.Row="1" Grid.Column="1" 
                     Background="Transparent" Height="35"
                     Foreground="White" FontSize="18" 
                     Width="70" HorizontalAlignment="Left"
                     Text="{Binding Path=Price, UpdateSourceTrigger=PropertyChanged, StringFormat=N2}"/>
        </Grid>
        <Button Grid.Row="1" Background="Transparent"
                Foreground="White" FontSize="16" 
                Width="100" Height="40"
                Content="Dodaj" BorderThickness="2" 
                BorderBrush="WhiteSmoke" Command="{Binding Path=AddProduct}"/>
        <Grid Grid.Row="2" Width="250">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Label Background="Transparent" Foreground="White" 
                   FontSize="16" Content="Avarage price:"
                   HorizontalAlignment="Center" VerticalAlignment="Center"/>
            <TextBlock Grid.Column="1" Background="Transparent" 
                       Foreground="White" FontSize="18" VerticalAlignment="Center"
                       Text="{Binding Path=AvaragePrice, UpdateSourceTrigger=PropertyChanged, StringFormat=N2}"/>
        </Grid>
    </Grid>
</Grid>

MainViewModel:

class MainViewModel : BaseModel
{
    MainModel mainModel;
    public MainViewModel()
    {
        // Инициализируем модель, подписываемся на изменение любого из её свойств
        mainModel = new MainModel();
        mainModel.PropertyChanged += (s, e) => { OnPropertyChanged(e.PropertyName); };          
    }
    public string Name
    {
        get
        {
            return mainModel.Name;
        }
        set
        {
            mainModel.Name = value;
            OnPropertyChanged();
        }
    }
    public double Price
    {
        get
        {
            return mainModel.Price;
        }
        set
        {
            mainModel.Price = value;
            OnPropertyChanged();
        }
    }
    public RelayCommand AddProduct
    {
        get
        {
            return mainModel.AddProduct;
        }
    }
    public ObservableCollection<Product> AllProducts
    {
        get
        {
            return mainModel.AllProducts;
        }
        set
        {
            mainModel.AllProducts = value;
            OnPropertyChanged();
        }
    }
    public double AvaragePrice
    {
        get
        {
            return mainModel.AvaragePrice;
        }
        set
        {
            mainModel.AvaragePrice = value;
            OnPropertyChanged();
        }
    }
}

MainModel:

class MainModel : BaseModel
{
    public MainModel()
    {
        allProducts = new ObservableCollection<Product>();   
        allProducts.CollectionChanged += (s,e) => { AvaragePrice = allProducts.Sum(x => x.Price) / allProducts.Count; };
    }
    string name;
    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
            OnPropertyChanged();
        }
    }
    double price;
    public double Price
    {
        get
        {
            return price;
        }
        set
        {
            price = value;
            OnPropertyChanged();
        }
    }
    ObservableCollection<Product> allProducts;
    public ObservableCollection<Product> AllProducts
    {
        get
        {
            return allProducts;
        }
        set
        {
            allProducts = value;      
            OnPropertyChanged();
            AvaragePrice = allProducts.Sum(x => x.Price) / allProducts.Count;
        }
    }
    RelayCommand addProduct;
    public RelayCommand AddProduct
    {
        get
        {
            return addProduct ?? (addProduct = new RelayCommand(obj =>
            {
                Product newProduct = new Product()
                {
                    Price = this.Price,
                    Name = this.Name
                };
                AllProducts.Add(newProduct);               
            }));
        }
    }
    double avaragePrice;
    public double AvaragePrice
    {
        get
        {
            return avaragePrice;
        }
        set
        {
            avaragePrice = value;
            OnPropertyChanged();
        }
    }
}
Answer 1

Смотрите в общем как можно поступить (за основу взял ответ с En SO)...

Подготовка

XAML разметка

Для примера нам (мне) понадобится набросать небольшой View:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <TextBlock Grid.Row="0" FontSize="16" Text="{Binding AvaragePrice, StringFormat={}В среднем: {0}$}" HorizontalAlignment="Center"/>
    <ListBox Grid.Row="1" ItemsSource="{Binding Items}" HorizontalContentAlignment="Stretch" BorderThickness="0">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid d:DataContext="{d:DesignInstance {x:Type local:ItemModel}}" >
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Column="0" Text="{Binding Name}"/>
                    <TextBox Grid.Column="1" Text="{Binding Price, StringFormat={}{0}$}"  BorderThickness="0"/>
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

INotifyPropertyChanged

Также нам понадобится INotifyPropertyChanged, для удобства создадим отдельный класс:

public class VM : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Реализация

Итак, скажем, у нас есть VM для наших предметов и некая модель самого предмета:

  • C моделью предмета думаю все понятно, просто объявляем необходимые свойства (в моем случае это имя и цена).

    public class ItemModel : VM
    {
        public string Name { get; set; }
        private int price;
        public int Price
        {
            get => price;
            set
            {
                price = value;
                OnPropertyChanged();
            }
        }
    }
    
  • В VM у нас пока будет все тоже, но реализуем коллекцию предметов и среднюю цену, а также, давайте сделаем метод, который будет обновлять цену:

    public class ItemsViewModel : VM
    {
        public ObservableCollection<ItemModel> Items { get; set; } = new ObservableCollection<ItemModel>();
        private int avaragePrice;
        public int AvaragePrice
        {
            get => avaragePrice;
            set
            {
                avaragePrice = value;
                OnPropertyChanged();
            }
        }
        public void UpdatePrice()
        {
            AvaragePrice = Items.Sum(x => x.Price) / Items.Count;
        }
    }
    

И так, теперь у вас есть выбор 1. Подписывать все ItemModel на событие изменение. 2. Использовать BindingList.

1. Используем ObservableCollection

  • Для начала подпишемся на событие изменения коллекции в нашей VM:

    public ItemsViewModel()
    {
        Items.CollectionChanged += ItemsOnCollectionChanged;
    }
    private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.OldItems != null)
        {
            foreach (INotifyPropertyChanged item in e.OldItems)
                item.PropertyChanged -= UpdatePrice;
        }
        if (e.NewItems != null)
        {
            foreach (INotifyPropertyChanged item in e.NewItems)
                item.PropertyChanged += UpdatePrice;
        }
    }
    
  • Изменим немного метод обновления:

    public void UpdatePrice(object sender, PropertyChangedEventArgs e)
    {
        AvaragePrice = Items.Sum(x => x.Price) / Items.Count;
    }
    

Вроде все... И так, что здесь происходит? Суть в следующем: ObservableCollection оповещает только если в коллекцию добавляется, либо что то удаляется. В этом случае мы при добавление предмета в коллекцию проходимся по всем его значениям и подписываемся на событие изменения, если наш предмет в коллекции реализует INotifyPropertyChanged. При удаление делаем обратное, то есть отписываемся. Таким образом, все Model внутри коллекции будут подписаны на событие обновление цены.

2. Используем BindingList

Есть довольно классная штука в WPF, как BindingList. У нее есть событие ListChanged, которое в свою очередь оповещает о любом (вроде) изменении в коллекции.

  • Перепишем ObservalCollection<ItemModel> на BindingList<ItemModel>.
  • В конструкторе подпишемся на событие изменения, ну и обновление цены можно положить внутрь, я думаю...:

    public ItemsViewModel()
    {
        Items.ListChanged += ItemsOnListChanged;
    }
    private void ItemsOnListChanged(object sender, ListChangedEventArgs e)
    {
        if (e.ListChangedType == ListChangedType.ItemChanged)
        {
            AvaragePrice = Items.Sum(x => x.Price) / Items.Count;
        }
    }
    public BindingList<ItemModel> Items { get; set; } = new BindingList<ItemModel>();
    

Ну тут, я думаю, все понятно и объяснять не нужно. Если у нас значение изменено, то обновляем цену (тут можете тип выбрать нужный).

В общем, результат у нас будет примерно следующий:

READ ALSO
Обход запрета Unity

Обход запрета Unity

Я написал большой скрипт используя SystemNumerics, ошибок в коде не было, Visual Studio его нормально воспринимал

125
Редактирование класса

Редактирование класса

В программе есть классВ нём имеются следующие строки:

223
Настройка RichTextBox

Настройка RichTextBox

Как известно, этот контрол ведет себя совсем не так, как обычный TextBox, особенно напрягают возможность вставки изображений посреди текста,...

164
Как проверить длину строки с помощью регулярного выражения?

Как проверить длину строки с помощью регулярного выражения?

Как этот код описать в регулярном выражении?

157