Search code examples
c#wpfxamlmvvm

WPF binded data in my ListBox is not updating


I am trying to create WPF application following MVVM pattern everithing was ok till i started binding commands changing the data. I don't understand why after the command executing info in my ListBox is not updating.

here is my ViewModel:

class FirstVM : ObservableObject
{
    private ObservableCollection<DetailScheme> _detailsScheme;
    private string _text;
    private ObservableCollection<Material> _materials;
    private ObservableCollection<Detail> _detail;

    public string Text
    {
        get { return _text; }
        set
        {
            _text = value;
            OnPropertyChanged();
        }
    }
    public ObservableCollection<DetailScheme> DetailScheme 
    { 
        get { return _detailsScheme; } 
    }
    
    public ObservableCollection<Material> Materials 
    {
        get { return _materials; } 
        set { 
                _materials = value;
                OnPropertyChanged();
            }
    }
    public ObservableCollection<Detail> Details 
    { 
        get { return _detail; } 
        set { 
                _detail = value; 
                OnPropertyChanged(); 
            }
    }
    public RelayCommand ProduceCommand { get; set; }

    public FirstVM () 
    {
        _detailsScheme = GetDetailsScheme("D:/ DetailsScheme.json");
        _materials = GetMaterials("D:/ Materials.json");
        _detail = GetDetails("D:/ Details.json");
        ProduceCommand = new RelayCommand(o => 
        {
                        //this method return new ObservableCollection
            Materials = GetMaterialsAfterNormalBatch(Materials, (o as DetailScheme).Materials);
                Text = Materials[0].ToString();
        });
        
    }

Material class:


    class Material : ObservableObject
    {
        private string _name;
        private int _amount;
    
        public int Amount { 
            get { return _amount; }
            set { 
                    _amount = value;
                    OnPropertyChanged();
                }  
            }
    
        public string Name { 
            get { return _name; }
            set { _name = value; }
        }
    
        public Material(string name, int amount)
        {
            _name = name;
            _amount = amount;
        }
        
        public static ObservableCollection<Material> GetMaterials(string path)
        {
            ObservableCollection<Material> materials;
            using (StreamReader sr = new StreamReader(path))
            {
                materials = JsonConvert.DeserializeObject<ObservableCollection<Material>>(sr.ReadToEnd());
            }
            return materials;
        }
        public static ObservableCollection <Material> GetMaterialsAfterNormalBatch(ObservableCollection<Material> allMaterials, ObservableCollection<Material> requiredMaterials)
        {
            for (int i = 0; i < allMaterials.Count; i++)
                for (int j = 0; j < requiredMaterials.Count; j++)
                    if (allMaterials[i].Name == requiredMaterials[j].Name)
                        allMaterials[i].Amount -= requiredMaterials[j].Amount;
            return allMaterials;
        }
        public override string ToString()
        {
            return Name+$" : {Amount}";
        }
    }

and View:

<UserControl x:Class="Storage_Manager.MVVM.View.Task_1"
             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:Storage_Manager.MVVM.View"
             xmlns:VM="clr-namespace:Storage_Manager.MVVM.ViewModel"
             xmlns:View="clr-namespace:Storage_Manager.MVVM.View"
             mc:Ignorable="d" 
             d:DesignHeight="515" d:DesignWidth="700">
        <UserControl.DataContext>
            <VM:FirstVM/>
        </UserControl.DataContext>
        <Grid>
            <ComboBox x:Name="BoxOfDetails"
                      Height="50" 
                      Width="250"  
                      SelectedIndex="0"
                      Margin="10,10,440,455"
                      ItemsSource="{Binding DetailScheme}">
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel>
                            <TextBlock Text="{Binding Name}" FontSize="18" />
                            <TextBlock Text="{Binding }" FontSize="9" Margin="0,7,0,0"/>
                        </StackPanel>
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>
            <Button Content ="Produce"
                    Width="200"
                    Margin="60,460,440,10"
                    Command="{Binding ProduceCommand}"
                    CommandParameter="{Binding ElementName=BoxOfDetails, Path=SelectedItem}"/>
            <ListBox ItemsSource="{Binding Materials}" 
                     Background="Transparent" 
                     Foreground="white"
                     BorderThickness="0"
                     IsHitTestVisible="False"/>
            <TextBlock Foreground="DarkGray"
               Text="{Binding Text}"/>
        </Grid>
    </UserControl>

I added TextBlock to see if the information is updating after click on the Button, and yes in my TextBlock data is changing after each click, but ListBox remains unchanged. Material class implements the INotifyPropertyChanged.


Solution

  • I think the trouble is that you don't bind to an Material item's property directly, but just let ToString() do the work, which is straight forward to get your data displayed, but lacks change notifications. A XAML only solution, leaving the C# code untouched, would be:

    <ListBox ItemsSource="{Binding Materials}" 
                         Background="Transparent" 
                         Foreground="white"
                         BorderThickness="0"                     
                         IsHitTestVisible="False">
        <ListBox.ItemTemplate>
            <DataTemplate>
                  <StackPanel Orientation="Horizontal">
                      <!-- ToDo tweak Widths by yourself according to you
                       material name and amout values widths, so that they look nicely aligned -->
                      <TextBlock MinWidth="150" Text={Binding Name, Mode=OneWay}" />
                      <TextBlock MinWidth="100" Text={Binding Amount, Mode=OneWay, StringFormat=N0}" 
                                 TextAlignment="Right" />
                  </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    

    You might as well add a "DisplayText" string property to Material class and have it return Name + Amount like ToString now does, and use it as DisplayMemberPath of the ListBox.

    Another solution would be to create a new ObservableCollectionin GetMaterialsAfterNormalBatch and not to return the allMaterials collection passed in. Then the UI update would go via CollectionChanged. But it is actually not a recommendation to recreate a new collection if only item member data changes.