Search code examples
c#wpfdata-bindingbindingprogress-bar

WPF C# Progressbar binding to Datagrid row


In my Datagrid there is a column in which there are Progressbars, each of which must change independently of the others when I refer to a particular row in a given Datagrid.

When trying to refer to a certain progressbar in a certain line nothing happens, I did so, in the hope that they will start to change at least all at once.

 private void ColorRow(System.Windows.Controls.DataGrid dg)
        {
            DataGridRow row = (DataGridRow)dg.ItemContainerGenerator.ContainerFromIndex(cmdvm.NumberOfFrame);
            
            if (row != null ) 
            { 
                if (cmdvm.NumberOfFrame > 0)
                {
                    row.Background = brush; 
                    cmdvm.PercentageOfFrame = registers[61];

                    ProgressBar progressbar = (ProgressBar)dg.FindName("progressbar");
                    progressbar.Value = registers[61];

                }
            }
        }

XAML:


        <DataGrid x:Name="Datagrid_CmdLines" SelectionUnit="Cell" HeadersVisibility="None" ItemsSource="{Binding CMD_Array}" Margin="52,308,0,0" AutoGenerateColumns="False" Width="635" Height="393" HorizontalAlignment="Left" VerticalAlignment="Top" Background="#FFEEEEEE" Grid.Row="1">
            <DataGrid.RowStyle>
                <Style TargetType="DataGridRow">
                    <Setter Property="IsHitTestVisible" Value="False"/>
                </Style>
            </DataGrid.RowStyle>
            <DataGrid.Columns>
                <DataGridTemplateColumn Width="150">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Grid>
                                <ProgressBar x:Name="progressbar" Value="{Binding PercentageOfFrame, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}}" Minimum="0" Maximum="100" Height="20"/>
                            </Grid>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>

Also my ViewModel:

        public class CMD_VM : INotifyPropertyChanged
        {
            private string[]? _cmd_array;
            private int _numberOfFrame;
            private int _percentage;

            public string[] CMD_Array
            {
                get
                {
                    return _cmd_array!;
                }
                set
                {
                    _cmd_array = value;
                    NotifyPropertyChanged("CMD_Array");
                }
            }
            
            public int NumberOfFrame
            {
                get
                {
                    return _numberOfFrame;
                }
                set
                {
                    _numberOfFrame = value;
                    NotifyPropertyChanged("NumberOfFrame");
                }
            }

            public int PercentageOfFrame
            {
                get
                {
                    return _percentage;
                }
                set
                {
                    _percentage = value;
                    NotifyPropertyChanged("PercentageOfFrame");
                }
            }

            public event PropertyChangedEventHandler? PropertyChanged;
            private void NotifyPropertyChanged(String propertyName)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }

How can I address each of the progressbars separately?


Solution

  • It looks like you misunderstood how WPF ItemsControl works. Whatever items you bind to the ItemsSource property will be the DataContext of the DataGridRow and DataGridTemplateColumn.CellTemplate.

    For example, if you bind your DataGrid to a collection of string items then each DataGridRow has the actual string value as DataContext of that row. This string item is also the DataContext of the DataTemplate of the DataGridTemplateColumn.CellTemplate.

    That's the reason why the binding on your ProgressBar is not working: string has no PercentageOfFrame property. This property is defined on the DataContext of the DataGrid (the CMD_VM view model class). The fixed binding must be:

    <ProgressBar x:Name="progressbar" 
                 Value="{Binding PercentageOfFrame, RelativeSource={RelativeSource AncestorType=DataGrid.DataContext}}" />
    

    Then update the progress value as follows:

    private void ColorRow(System.Windows.Controls.DataGrid dg)
    {
      // You must check the index BEFORE you use it and not afterwards!
      if (cmdvm.NumberOfFrame <= 0) 
      {
        return;
      }
    
      // This line of code has potential to fail: 
      // DataGrid uses row virtualization. If the requested row container is not visible,
      // the ItemContainerGenerator will return NULL!
      // Instead of handling row containers you should introduce row item models 
      // that bind to the row container. This way this code will always work
      DataGridRow row = (DataGridRow)dg.ItemContainerGenerator.ContainerFromIndex(cmdvm.NumberOfFrame);            
      if (row != null) 
      { 
        row.Background = brush; 
     
        // Because of the data binding set in the XAML updating the source 
        // property will update the ProgressBar
        cmdvm.PercentageOfFrame = registers[61];
      }
    }
    

    However, you likely don't want to bind each row's ProgressBar to the same PercentageOfFrame property. Updating the property would modify the PropregssBar on all rows simultaneously. It's also not necessary to explicitly set the ProgressBar.Value from your code when you have assigned a Binding to this property. Setting this property explicitly would remove the Binding and therefore break your code. Instead, update the source property PercentageOfFrame. The binding will send the new value to the ProgressBar.

    As suggested by others, you must introduce a data model. This is how any ItemsControl in WPF should be used.

    RowItem.cs

    public class RowItem : INotifyPropertyChanged
    {
      public event PropertyChangedEventHandler? PropertyChanged;
      private int _percentage;
      private Color _color;
    
      public string Value { get; private set; }
    
      public Color Color
      {
        get => _color;
        set
        {
          _color = value;
          NotifyPropertyChanged();
        }
      }
    
      public int PercentageOfFrame
      {
        get => _percentage;
        set
        {
          _percentage = value;
          NotifyPropertyChanged();
        }
      }
    
      public RowItem(string value)
        => Value = value;
    
      private void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
        => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    

    CMD_VM.cs

    public class CMD_VM : INotifyPropertyChanged
    {
      public event PropertyChangedEventHandler? PropertyChanged;
      public ObservableCollection<RowItem> RowItems { get; }
    
      // If this is a per-row property then move it to the RowItem class
      private int _numberOfFrame;
      public int NumberOfFrame
      {
        get => _numberOfFrame;
        set
        {
          _numberOfFrame = value;
          NotifyPropertyChanged();
        }
      }
    
      public CMD_VM()
      {
        // Initialize the read-only collection.
        // Do not replace the collection instance (bad perfromance).
        // Instead add/remove items to/from the collection.
        this.RowItems = GetRowItems();
      }
    
      private void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
        => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    

    Set the row's propgress:

    private void ColorRow()
    {
      // Are you sure that you want to check if the index NumberOfFrame 
      // is > 0 (and not >= 0)?
      // In your original code you were checking whether the index is > 0.
      // '0' is a valid index. To invalidate cmdvm.NumberOfFrame, this 
      // property should be set to `-1` and not '0'. 
      // Then the condition would change to: cmdvm.NumberOfFrame >= 0 
    
      if (cmdvm.NumberOfFrame > 0 
        && cmdvm.NumberOfFrame < cmdvm.RowItems.Count)
      {
        RowItem row = cmdvm.RowItems[cmdvm.NumberOfFrame];
          
        // Assign Color objects and not Brush objects
        row.Color = Colors.Red;
          
        row.PercentageOfFrame = registers[61];
      }
    }
    

    The properly bind the DataGrid to the new data source and the column's CellTemplate to the data model:

    <DataGrid ItemsSource="{Binding RowItems}">
      <DataGrid.RowStyle>
        <Style TargetType="DataGridRow">
    
          <!-- Bind the row's Background to the RowItem (the DataContext of the DataGridRow) -->
          <Setter Property="Background">
            <Setter.Value>
              <SolidColorBrush Color="{Binding Color}" />
            </Setter.Value>
          </Setter>
    
          <Setter Property="IsHitTestVisible"
                  Value="False" />
        </Style>
      </DataGrid.RowStyle>
    
      <DataGrid.Columns>
        <DataGridTemplateColumn Width="150">
          <DataGridTemplateColumn.CellTemplate>
            <DataTemplate DataType="{x:Type RowItem}">
              <ProgressBar Value="{Binding PercentageOfFrame}"
                           Minimum="0"
                           Maximum="100"
                           Height="20" />
            </DataTemplate>
          </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
      </DataGrid.Columns>
    </DataGrid>