c# - example - wpf datagridtemplatecolumn textbox




DataGridTemplateColumn(ComboBox, DatePicker) Resets/Clears and doesn't fire AddingNewItem (4)

EDIT - Added code to make one-click editing possible.

  1. Changed all column bindings with UpdateSourceTrigger=PropertyChanged - This is because the default value of LostFocus works at a row level, not cell level, which means that you have to leave the row completely before the bindings take effect. This works ok for many situations, but not when you have two columns bound to the same property, because the changes done to one of those columns won't show inmediately in the other column.
  2. Set IsHitTestVisible="False" to the non-editing template of the central column - My first approach was to make the column read-only and use only the CellTemplate... But this didn't trigger the AddingNewItem event. It seems you NEED to change from the regular cell to the editing cell for that event to fire, but since your non-editing template is not what you want the user to interact with, disabling hit testing makes all sense. That way you force the user to change to edit mode, hence triggering the event, before being able to enter input.
  3. Handled the CurrentCellChanged event of the DataGrid. In the handler, use the methods CommitEdit() to make sure the previously selected cell leaves editing mode, and an asynchronous call to BeginEdit() to start editing the current cell right away, without having to wait for a second click.
  4. Handled the Loaded event of the DatePickers inside the CellEditingTemplates. In the handler, used Keyboard.Focus() to give focus to the DatePicker as soon as it is loaded, saving the user the need to click a third time to put the focus on the control.

XAML:

<Grid>
    <DataGrid x:Name="dg" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="299" AutoGenerateColumns="False" Width="497" AddingNewItem="dg_AddingNewItem" CanUserAddRows="True"
              CurrentCellChanged="dg_CurrentCellChanged">
        <DataGrid.Columns>
            <DataGridTemplateColumn Header="DateWorks">
                <DataGridTemplateColumn.CellEditingTemplate>
                    <DataTemplate>
                        <DatePicker Loaded="DatePicker_Loaded" 
                                    SelectedDate="{Binding InvoiceDate,
                                                           UpdateSourceTrigger=PropertyChanged}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellEditingTemplate>
            </DataGridTemplateColumn>
            <DataGridTemplateColumn Header="DateDoesn'tWork">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <DatePicker IsHitTestVisible="False" 
                                    SelectedDate="{Binding InvoiceDate,
                                                           UpdateSourceTrigger=PropertyChanged}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
                <DataGridTemplateColumn.CellEditingTemplate>
                    <DataTemplate>
                        <DatePicker Loaded="DatePicker_Loaded" 
                                    SelectedDate="{Binding InvoiceDate, 
                                                           UpdateSourceTrigger=PropertyChanged}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellEditingTemplate>
            </DataGridTemplateColumn>
            <DataGridTextColumn Header="Text" Binding="{Binding Description, 
                                                                UpdateSourceTrigger=PropertyChanged}"/>
        </DataGrid.Columns>
    </DataGrid>
</Grid>

Code-behind:

private void dg_CurrentCellChanged(object sender, EventArgs e)
{
    var dataGrid = sender as DataGrid;

    dataGrid.CommitEdit();
    Dispatcher.BeginInvoke(new Action(() => dataGrid.BeginEdit()), System.Windows.Threading.DispatcherPriority.Loaded);
}

private void DatePicker_Loaded(object sender, RoutedEventArgs e)
{
    Keyboard.Focus(sender as DatePicker);
}

I've narrowed down the problem to the following example that has a DataGrid with three columns.

XAML:

<Window x:Class="DataGridColumnTemplate_NotFiringAddingNewItem.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid x:Name="dg" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="299" AutoGenerateColumns="False" Width="497" AddingNewItem="dg_AddingNewItem" CanUserAddRows="True">
            <DataGrid.Columns>
                <DataGridTemplateColumn Header="DateWorks">
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <DatePicker SelectedDate="{Binding InvoiceDate}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
                <DataGridTemplateColumn Header="DateDoesn'tWork">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <DatePicker SelectedDate="{Binding InvoiceDate}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <DatePicker SelectedDate="{Binding InvoiceDate}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
                <DataGridTextColumn Header="Text" Binding="{Binding Description}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

C#:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        List<JobCostEntity> l = new List<JobCostEntity>()
        { 
            new JobCostEntity() { Id = 0, InvoiceDate = DateTime.Now, Description = "A"},
            new JobCostEntity() { Id = 0, InvoiceDate = DateTime.Now, Description = "B"}
        };

        dg.ItemsSource = l;
    }
    private void dg_AddingNewItem(object sender, AddingNewItemEventArgs e)
    {
        MessageBox.Show("AddingNewItem");
    }
}

public partial class JobCostEntity
{
    public int Id { get; set; }
    public int JobId { get; set; }
    public Nullable<int> JobItemId { get; set; }
    public Nullable<System.DateTime> InvoiceDate { get; set; }
    public Nullable<System.DateTime> ProcessedDate { get; set; }
    public int PackageId { get; set; }
    public int DelegateId { get; set; }
    public string Description { get; set; }
    public Nullable<decimal> LabourCost { get; set; }
    public Nullable<decimal> PlantOrMaterialCost { get; set; }
    public Nullable<decimal> SubcontractorCost { get; set; }
    public Nullable<decimal> TotalCost { get; set; }
    public bool Paid { get; set; }
}

If the first column you click on in the new item row is 'DateWorks' or 'Text', then you will raise the AddingNewItem event.

If instead you click the 'DateDoesntWork' column first, you can select a date, but no new item is added until you move to one of the other columns, at which point the value in the 'DateDoesntWork' DatePicker gets cleared.

What on earth is going on?


It's arguably(!) desirable to have the DatePicker already visible to the user (hence both a CellTemplate and a CellEditingTemplate), rather than them have to click the cell to 'reveal' the control.

Is there some way I have to inform the DataGrid that my DataGridTemplateColumn Control has just set a value on a new row? If so, how so?!


EDIT:

Inspired by this post: https://social.msdn.microsoft.com/Forums/vstudio/en-US/93d66047-1469-4bed-8fc8-fa5f9bdd2166/programmatically-beginning-edit-in-datagrid-cell?forum=wpf

I have tried to hack my way around the problem by adding the following to the 'DateDoesntWork' column DatePicker, which does cause the AddingNewItem event to fire, but the selected date still doesn't get added to the underlying entity.

private void DatePicker_GotFocus(object sender, RoutedEventArgs e)
{
    if (dg.SelectedIndex == dg.Items.Count - 1)
    {
        DataGridCellInfo dgci = dg.SelectedCells[0];
        DataGridCell dgc = DataGridHelper.GetCell(dg, GetRowIndex(dg, dgci), GetColIndex(dg, dgci));
        dgc.Focus();
        dg.BeginEdit();
    }

}

It seems like the DatePicker is still trying to target the NewItemPlaceholder, if that makes any sense?!


Stranger still, if you select a date in the DateDoesntWork column on the new row, then start editing the Text column on the new row, then without entering any text, select the row above ... now another new row is added and that newly added row shows the date i selected for the row before!!!

Total. Madness.


As Maxime Tremblay-Savard has metioned, it seems like the CellTemplate is blocking the 'layer' below and stopping the AddingNewItem event firing, though the built in DataGridColumn types don't suffer from this problem.


First part of the code is only to show the date in 'Working column". To fix the click twice to edit, then you can use the helper class.

Hope it helps...

<Window x:Class="WpfApplicationAnswerFor.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid x:Name="dg" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="299" AutoGenerateColumns="False" Width="497" AddingNewItem="dg_AddingNewItem" CanUserAddRows="True">
            <DataGrid.Columns>
                <DataGridTemplateColumn Header="DateWorks">

                    <!-- Here -->
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding InvoiceDate, StringFormat='d'}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>

                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <DatePicker SelectedDate="{Binding InvoiceDate}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
                <DataGridTemplateColumn Header="DateDoesn'tWork">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <DatePicker SelectedDate="{Binding InvoiceDate}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <DatePicker SelectedDate="{Binding InvoiceDate}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
                <DataGridTextColumn Header="Text" Binding="{Binding Description}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

Fix for single click edit:

Usage:

<Window ...
        xmlns:WpfUtil="clr-namespace:HQ.Util.Wpf.WpfUtil;assembly=WpfUtil">

<DataGrid ... util:DataGridCellHelper.IsSingleClickInCell="True">

Class

using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace HQ.Wpf.Util
{
    public static class DataGridCellHelper
    {
        #region IsSingleClickInCell
        public static readonly DependencyProperty IsSingleClickInCellProperty =
            DependencyProperty.RegisterAttached("IsSingleClickInCell", typeof(bool), typeof(DataGrid), new FrameworkPropertyMetadata(false, OnIsSingleClickInCellSet)); public static void SetIsSingleClickInCell(UIElement element, bool value) { element.SetValue(IsSingleClickInCellProperty, value); }

        public static bool GetIsSingleClickInCell(UIElement element)
        {
            return (bool)element.GetValue(IsSingleClickInCellProperty);
        }

        private static void OnIsSingleClickInCellSet(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (!(bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
            {
                if ((bool)e.NewValue)
                {
                    var dataGrid = sender as DataGrid;
                    Debug.Assert(dataGrid != null);
                    EventManager.RegisterClassHandler(typeof(DataGridCell),
                        DataGridCell.PreviewMouseLeftButtonUpEvent,
                        new RoutedEventHandler(OnPreviewMouseLeftButtonDown));
                }
            }
        }

        private static void OnPreviewMouseLeftButtonDown(object sender, RoutedEventArgs e)
        {
            DataGridCell cell = sender as DataGridCell;
            if (cell != null && !cell.IsEditing && !cell.IsReadOnly)
            {
                var checkBoxes = ControlHelper.FindVisualChildren<CheckBox>(cell);
                if (checkBoxes != null && checkBoxes.Count() > 0)
                {
                    foreach (var checkBox in checkBoxes)
                    {
                        if (checkBox.IsEnabled)
                        {
                            checkBox.Focus();
                            checkBox.IsChecked = !checkBox.IsChecked;
                            var bindingExpression = checkBox.GetBindingExpression(CheckBox.IsCheckedProperty); if (bindingExpression != null) { bindingExpression.UpdateSource(); }
                        }
                        break;
                    }
                }
            }
        }
        #endregion
    }
}

In case you need a solution for your InvoiceDate, here is a way to have the behaviour you describe for DateWorks by creating a DataGridDateColumn like so:

public class DataGridDateColumn : DataGridBoundColumn
{
    public string DateFormatString { get; set; }

    protected override void CancelCellEdit(FrameworkElement editingElement, object before)
    {
        var picker = editingElement as DatePicker;
        if (picker != null)
        {
            picker.SelectedDate = DateTime.Parse(before.ToString());
        }
    }

    protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
    {
        var element = new DatePicker();

        var binding = new Binding(((Binding)this.Binding).Path.Path) {Source = dataItem};
        if (DateFormatString != null)
        {
            binding.Converter = new DateTimeConverter();
            binding.ConverterParameter = DateFormatString;
        }
        element.SetBinding(DatePicker.SelectedDateProperty, this.Binding);

        return element;
    }

    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
        var element = new TextBlock();

        var b = new Binding(((Binding) Binding).Path.Path) {Source = dataItem};
        if (DateFormatString != null)
        {
            b.Converter = new DateTimeConverter();
            b.ConverterParameter = DateFormatString;
        }

        element.SetBinding(TextBlock.TextProperty, b);
        return element;
    }

    protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
    {
        var element = editingElement as DatePicker;
        if (element != null)
        {
            if (element.SelectedDate.HasValue ) return element.SelectedDate.Value;
        }
        return DateTime.Now;
    }
}

public class DateTimeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var date = (DateTime)value;
        return date.ToString(parameter.ToString());
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        DateTime resultDateTime;
        if (DateTime.TryParse(value.ToString(), out resultDateTime))
        {
            return resultDateTime;
        }
        return value;
    }
}

I then added two more columns to your grid:

<custom:DataGridDateColumn Header="Custom" Binding="{Binding InvoiceDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>  
<DataGridCheckBoxColumn Header="Paid" Binding="{Binding Paid}"/>

If I click into the Custom field now, I get the Message Box, select a date and then tab out, the value gets cleared until I implement INPC on InvoiceDate:

    private Nullable<System.DateTime> _invoiceDate;
    public Nullable<System.DateTime> InvoiceDate
    {
        get { return _invoiceDate; }
        set
        {
            _invoiceDate = value;
            OnPropertyChanged();
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }

Now, the date is showing according to the DateFormatString set.

Again, I am aware that does not answer your original question, but after my hasty comment from before, I felt obliged to at least come up with a specific workaround.


My take on the issue. The issue you're having with your second column is with the DataGridTemplateColumn. The DataGridTemplateColumn is the actual column, so it's where you should click to add a new line, when you put a control in a DataTemplate in the DataGridCTemplateColumn.CellTemplate, it becomes a "layer" above it. The controls in this "upper layer" are then usable without actually clicking on the Row, which means it does not create a new line.


I did some testing to prove this, if you create a checkbox column this way:

<DataGridCheckBoxColumn Header="Paid" Binding="{Binding Paid}"/>

If you click on the checkbox, it triggers the event to add a new line because this is the actual column, not a control over it.

But if you do the same but with the DataGridTemplateColumn, like this:

<DataGridTemplateColumn>
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <CheckBox Content="Paid" IsChecked="{Binding Paid}" Margin="5"></CheckBox>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

Note the margin, to be able to click on the actual cell and not on the control above the cell

With this way, if you click on the cell itself, it will trigger the add a new line event, while if you click on the checkbox that is "above" the cell, it will not trigger the event and will only check/uncheck it.


There is also a remark on the msdn documentation that might help you understand also:

The DataGridTemplateColumn type enables you to create your own column types by specifying the cell templates used to display values and enable editing. Set the CellTemplate property to specify the contents of cells that display values, but do not allow editing. Set the CellEditingTemplate property to specify the contents of cells in editing mode. If you set the column IsReadOnly property to true, the CellEditingTemplate property value is never used.

I hope this gives you a better insight on what's going on with your DataGrid

EDIT

Something like this would permit you to manually add the line when you click "Enter" after selectionning your date.

private void DatePicker_KeyUp(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                List<JobCostEntity> tempList = (List<JobCostEntity>)dg.ItemsSource;
                tempList.Add(new JobCostEntity() { InvoiceDate = ((DatePicker)sender).DisplayDate });
                dg.ItemsSource = tempList;
            }
        }






datagridtemplatecolumn