Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 623
3-14159265358979323846264 Avatar asked Jun 28 '15 11:06

3-14159265358979323846264


1 Answers

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.

like image 183
mindandmedia Avatar answered Oct 02 '22 01:10

mindandmedia