11

I'm still fighting with manipulation of cell backgrounds so I'm asking a new question.

A user "H.B." wrote that I can actually set the cell style during the AutoGeneratingColumn event - Change DataGrid cell colour based on values. The problem is that I'm not sure how to do it.

What I want: Set different background colours for each cell depending on its value. If the value is null I also want it not to be clickable (focusable I guess).

What I have / I'm trying to do:

private void mydatagrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
    foreach (Cell cell in e.Column)
    {
        if (cell.Value < 1)
        { 
            cell.Background = Color.Black; 
            cell.isFocusable = false; 
        } 
        else
        {
            cell.Background = Color.Pink;
        }
    }
}

This is just the pseudocode. Is something like this is possible during column auto-generation and if so, how can I edit my code so it will be valid?

I read about value convertors but I want to know if it's somehow possible programmatically, without writing XAML.

Please understand that I'm still a beginner to C#/WPF/DataGrid.

Solution part1:

I used the answer I accepted. Just put it into

<Window.Resources> 
<local:ValueColorConverter x:Key="colorConverter"/>
        <Style x:Key="DataGridCellStyle1" TargetType="{x:Type DataGridCell}"> 
            <Setter Property="Padding" Value="5"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type DataGridCell}">
                        <Border Padding="{TemplateBinding Padding}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
                            <Border.Background>
                                <MultiBinding Converter="{StaticResource colorConverter}">
                                    <Binding RelativeSource="{RelativeSource AncestorType=DataGridCell}" Path="Content.Text"/>
                                    <Binding RelativeSource="{RelativeSource AncestorType=DataGridCell}" Path="IsSelected"/>
                                </MultiBinding>
                            </Border.Background>
                            <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
</Window.Resources>

And made for it a MultiBinding convertor so I can also set the background color for selected cells.

Problem:

Now I only have to solve the problem of setting focus of empty cells. Any hints?

  <Style.Triggers>
        <Trigger Property="HasContent" Value="False">
            <Setter Property="Focusable" Value="False"/>
        </Trigger>
    </Style.Triggers>

This doesn't work. I had empty strings in the empty cells, but they are filled with ´null´s so it should work, right? Or what am I doing wrong :| ?

Solution part 2:

So the code above won't work as long as the cell value is a ´TextBox´ so I decided to find another way to deal with it which can be found in my answer here: https://stackoverflow.com/a/16673602/2296407

Thanks for trying to help me :)

Community
  • 1
  • 1
Ms. Nobody
  • 1,219
  • 3
  • 14
  • 34
  • you can try this in rowdatabound event of grid. http://stackoverflow.com/questions/3146456/how-can-i-change-the-background-color-of-a-gridview-cell-based-on-a-conditional – Waqar Majid May 20 '13 at 09:34
  • 1
    Auto-generated cell, certainly, has "content" - it is (that's why I used Content.Text in my answer) :) Try using – morincer May 21 '13 at 10:44
  • @morincer Thanks for your help. I managed to work with the empty cells other way cause the DataTrigger didn't want to work with me and I was already very mad :D. But really thx for the support. – Ms. Nobody May 21 '13 at 15:24
  • @goodfriend, no problems. Added some code-style comment to your answer though – morincer May 21 '13 at 17:52
  • It's more like fake code = pseudo code :) (I do have the same problem though, but im looking for a solution in codebehind – VisualBean Nov 05 '13 at 11:44

3 Answers3

14

I can propose two different solutions for your question: the first is "code-behind-style" (which you are asking for but personally I think it is not right approach in WPF) and more WPF-style (which more tricky but keeps code-behind clean and utilizes styles, triggers and converters)

Solution 1. Event handling and code-behind logic for coloring

First of all, the approach you've chosen will not work directly - the AutoGeneratingColumn event is meant to be used for altering the entire column appearance, not on the cell-by-cell basis. So it can be used for, say, attaching the correct style to entire column basing on it's display index or bound property.

Generally speaking, for the first time the event is raised your datagrid will not have any rows (and consequently - cells) at all. If you really need to catch the event - consider your DataGrid.LoadingRow event instead. And you will not be able to get the cells that easy :)

So, what you do: handle the LoadingRow event, get the row (it has the Item property which holds (surprisingly :)) your bound item), get the bound item, make all needed calculations, get the cell you need to alter and finally alter the style of the target cell.

Here is the code (as item I use a sample object with the int "Value" property that I use for coloring).

XAML

   <DataGrid Name="mygrid" ItemsSource="{Binding Items}" AutoGenerateColumns="True" LoadingRow="DataGrid_OnLoadingRow"/>

.CS

    private void DataGrid_OnLoadingRow(object sender, DataGridRowEventArgs e)
    {
        Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() => AlterRow(e)));
    }

    private void AlterRow(DataGridRowEventArgs e)
    {
        var cell = GetCell(mygrid, e.Row, 1);
        if (cell == null) return;

        var item = e.Row.Item as SampleObject;
        if (item == null) return;

        var value = item.Value;

        if (value <= 1) cell.Background = Brushes.Red;
        else if (value <= 2) cell.Background = Brushes.Yellow;
        else cell.Background = Brushes.Green;
    }

    public static DataGridRow GetRow(DataGrid grid, int index)
    {
        var row = grid.ItemContainerGenerator.ContainerFromIndex(index) as DataGridRow;

        if (row == null)
        {
            // may be virtualized, bring into view and try again
            grid.ScrollIntoView(grid.Items[index]);
            row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(index);
        }
        return row;
    }

    public static T GetVisualChild<T>(Visual parent) where T : Visual
    {
        T child = default(T);
        int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < numVisuals; i++)
        {
            var v = (Visual)VisualTreeHelper.GetChild(parent, i);
            child = v as T ?? GetVisualChild<T>(v);
            if (child != null)
            {
                break;
            }
        }
        return child;
    }

    public static DataGridCell GetCell(DataGrid host, DataGridRow row, int columnIndex)
    {
        if (row == null) return null;

        var presenter = GetVisualChild<DataGridCellsPresenter>(row);
        if (presenter == null) return null;

        // try to get the cell but it may possibly be virtualized
        var cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
        if (cell == null)
        {
            // now try to bring into view and retreive the cell
            host.ScrollIntoView(row, host.Columns[columnIndex]);
            cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
        }
        return cell;

    }

Solution 2. WPF-style

This solution uses code-behind only for value-to-color convertions (assuming that that logic of coloring is more complex than equality comparison - in that case you can use triggers and do not mess with converters).

What you do: set DataGrid.CellStyle property with style that contains a data trigger, which checks if the cell is within a desired column (basing on it's DisplayIndex) and if it is - applies background through a converter.

XAML

<DataGrid Name="mygrid" ItemsSource="{Binding Items}" AutoGenerateColumns="True">
        <DataGrid.Resources>
            <local:ValueColorConverter x:Key="colorconverter"/>
        </DataGrid.Resources>
        <DataGrid.CellStyle>
            <Style TargetType="DataGridCell">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Column.DisplayIndex}" Value="1">
                        <Setter Property="Background" Value="{Binding RelativeSource={RelativeSource Self}, Path=Content.Text, Converter={StaticResource colorconverter}}"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </DataGrid.CellStyle>
    </DataGrid>

.CS

public class ValueColorConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var str = value as string;
        if (str == null) return null;

        int intValue;
        if (!int.TryParse(str, out intValue)) return null;

        if (intValue <= 1) return Brushes.Red;
        else if (intValue <= 2) return Brushes.Yellow;
        else return Brushes.Green;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

UPD: If you need to color entire datagrid, XAML is much easier (no need to use triggers). Use the following CellStyle:

    <DataGrid.CellStyle>
            <Style TargetType="DataGridCell">
                 <Setter Property="Background" Value="{Binding RelativeSource={RelativeSource Self}, Path=Content.Text, Converter={StaticResource colorconverter}}"/>
            </Style>
    </DataGrid.CellStyle>
morincer
  • 884
  • 6
  • 6
  • 1
    You should rather implement a "is-less-than-converter" or similar, as `IsFocusable` should be changed as well. – H.B. May 20 '13 at 10:34
  • Thanks for this very long and helpful answer. The converter is working good I think but those cells just don't want to get coloured. I tried editing the setting and also I'm working with double values there. - edited it a bit. But still. I'm not friendly enough with databinding. Do u think there might be a problem? I'm filling the datagrid in my c# code from dataset with datatable. I'm getting the values in convertor correctly but it just won't color my cells background :'( – Ms. Nobody May 20 '13 at 11:51
  • Would you plz post a piece of code you're using for binding the grid and coloring it (XAML + code-behind) so I can check what's going wrong? – morincer May 20 '13 at 12:29
  • Ok, just don't laugh if there is something silly. But the thing is that the stuff I need in datagrid is not always same amount (number of rows/columns) so I'm filling a datatable with that stuff and putting that table into dataset just because it somehow works this way. I always fill the datagrid with data with a button click using this code: `mydatagrid.ItemsSource = dataset.Tables[0].DefaultView;` it works but I'm not sure how to connect this kind of filling the grid with the setup of background color. I don't write anything about binding in the xaml just do this and it works. – Ms. Nobody May 20 '13 at 12:37
  • I'm using the code u posted. Just replaced int with double everywhere cause my values are doubles. I just don't really understand how should the code in XAML work. I get the trigger but I don't understand how binding works. The one in . Could u help me understand it somehow, please? – Ms. Nobody May 20 '13 at 13:19
  • Well, it just checks if DisplayIndex of the column, associated with the current cell, is 1 (BTW if you're coloring not the second column - it won't work :)) and if it is - it assigns a value of the cell to the background through a converter. If you need to color more than one column or you need to color all the columns - the code shall be different. – morincer May 20 '13 at 13:28
  • Damn, I want to color whole datagrid :D – Ms. Nobody May 20 '13 at 13:31
  • Updated the answer with the case of full grid coloring – morincer May 20 '13 at 15:27
  • Ok, thx it didn't work at first but it works the way I placed in question. Thanks everyone for helping. – Ms. Nobody May 21 '13 at 10:29
  • Great answer, +1. Thanks. – Ricky Aug 14 '18 at 13:17
  • Great answer, thank you very much! (+1) Can you tell me, where do you get this specific information from, that there (e.g. solution WPF) is a field like "Column.DisplayIndex" in a DataGridCell? I was searching for almost one hour and I couldn't even "see" this field inside the VS Visual Tree. – SKiD Mar 15 '19 at 12:00
1

What i meant is that you can set the CellStyle property of the column, you can not manipulate cells directly as they are not available in this event. The style can contain your conditional logic in the form of DataTriggers (will need a converter as you have "less-than" and not equals) and Setters.

Also if the logic is not specific to the columns you can set the style globally on the grid itself. The point of using the event would be to manipulate the column properties which you can not access otherwise.

H.B.
  • 166,899
  • 29
  • 327
  • 400
-2

I am not sure whether this property (Cell.Style) is available in your WPF Datagrid. Probably some alternative exists in your case. It has worked for WinForms datagrid.

 cell.Style.BackColor = System.Drawing.Color.Black;
KbManu
  • 420
  • 3
  • 7