Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to set background of a datagrid cell during AutoGeneratingColumn event depending on its value?

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 :)

like image 996
Ms. Nobody Avatar asked May 20 '13 08:05

Ms. Nobody


1 Answers

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>
like image 53
morincer Avatar answered Sep 20 '22 21:09

morincer