Подтвердить что ты не робот

DataGridTemplateColumn (ComboBox, DatePicker) Сбрасывает/очищает и не запускает AddingNewItem

Я сузил проблему до следующего примера с DataGrid с тремя столбцами.

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>

С#:

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; }
}

Если первый столбец, который вы нажимаете в строке нового элемента, - "DateWorks" или "Текст", вы поднимете событие AddingNewItem.

Если вместо этого вы сначала нажмете столбец "DateDoesntWork", вы можете выбрать дату, но новый элемент не будет добавлен до тех пор, пока вы не перейдете к одному из других столбцов, после чего значение в "DateDoesntWork" DatePicker будет очищено.

Что происходит?


Возможно, (!) желательно, чтобы DatePicker уже был видимым пользователю (следовательно, и CellTemplate и CellEditingTemplate), а не им нужно щелкнуть ячейку, чтобы "открыть" элемент управления.

Есть ли способ сообщить DataGrid, что мой DataGridTemplateColumn Control только что установил значение в новой строке? Если да, то как??


EDIT:

Вдохновленный этой записью: https://social.msdn.microsoft.com/Forums/vstudio/en-US/93d66047-1469-4bed-8fc8-fa5f9bdd2166/programmatically-beginning-edit-in-datagrid-cell?forum=wpf

Я попытался взломать проблему, добавив следующее в столбец DateDoesntWork DatePicker, который вызывает событие AddingNewItem, но выбранная дата до сих пор не добавляется в базовый объект.

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();
    }

}

Кажется, что DatePicker все еще пытается настроить таргетинг на NewItemPlaceholder, если это имеет смысл?!


Незнакомец, если вы выберете дату в столбце DateDoesntWork в новой строке, затем начните редактирование столбца "Текст" в новой строке, затем без ввода какого-либо текста выберите строку выше... теперь добавляется еще одна новая строка и что новая добавленная строка показывает дату, выбранную для строки до этого.

Total. Безумие.


Как отметил Максим Тремблей-Савард, похоже, что CellTemplate блокирует "уровень" ниже и останавливает запуск события AddingNewItem, хотя встроенные типы DataGridColumn не страдают от этой проблемы.

4b9b3361

Ответ 1

Если вы используете элемент управления, который обрабатывает щелчок мыши в CellTemplate, DataGrid никогда не получает событие click, которое запускает его для переключения в режим редактирования. Так, например, упомянутый eoinmullan, решение состоит в том, чтобы установить контроль IsHitTestVisible = False. Ниже приведен рабочий код. Я добавил INotifyPropertyChanged, чтобы мы могли увидеть измененное значение, отраженное в пользовательском интерфейсе. Я также добавил красный фон для DateDoesn'tWork CellTemplate, поэтому вы можете видеть, когда DataGrid переходит из режима отображения в режим редактирования.

<Window x:Class="WpfApplication1.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.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding InvoiceDate}"/>

                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
                <DataGridTemplateColumn.CellEditingTemplate>
                    <DataTemplate>
                        <DatePicker SelectedDate="{Binding InvoiceDate}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellEditingTemplate>
            </DataGridTemplateColumn>
            <DataGridTemplateColumn Header="DateDoesn'tWork">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate >
                        <!--Differentiate visually between CellTemplate and CellEditTemplate by using red background-->
                        <DatePicker Background="Red" IsHitTestVisible="False" 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>

 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 : INotifyPropertyChanged
{
    private string _description;
    private DateTime? _invoiceDate;

    public int Id { get; set; }
    public int JobId { get; set; }
    public Nullable<int> JobItemId { get; set; }

    public Nullable<System.DateTime> InvoiceDate
    {
        get { return _invoiceDate; }
        set
        {
            if (value.Equals(_invoiceDate)) return;
            _invoiceDate = value;
            OnPropertyChanged();
        }
    }

    public Nullable<System.DateTime> ProcessedDate { get; set; }
    public int PackageId { get; set; }
    public int DelegateId { get; set; }

    public string Description
    {
        get { return _description; }
        set
        {
            if (value == _description) return;
            _description = value;
            OnPropertyChanged();
        }
    }

    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; }

    public event PropertyChangedEventHandler PropertyChanged;


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

Ответ 2

Мой вопрос. Проблема, с которой вы столкнулись со вторым столбцом, - с DataGridTemplateColumn. DataGridTemplateColumn - это фактический столбец, поэтому там, где вы должны щелкнуть, чтобы добавить новую строку, когда вы поместите элемент управления в DataTemplate в DataGridCTemplateColumn.CellTemplate, он станет "слоем" над ним. Элементы управления в этом "верхнем слое" затем можно использовать без фактического нажатия на строку, что означает, что она не создает новую строку.


Я сделал некоторое тестирование, чтобы доказать это, если вы создадите столбец флажка таким образом:

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

Если вы установите флажок, это событие добавит новую строку, потому что это фактический столбец, а не контроль над ним.

Но если вы сделаете то же самое, но с DataGridTemplateColumn, вот так:

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

Обратите внимание на маржу, чтобы иметь возможность нажимать на действительную ячейку, а не на элемент управления над ячейкой

Таким образом, если вы щелкнете по самой ячейке, она вызовет добавление нового события линии, а если вы нажмете на флажок, который находится "над" ячейкой, он не инициирует событие и будет проверять только/снимите флажок.


Также есть примечание к документации msdn, которая может помочь вам понять:

Тип DataGridTemplateColumn позволяет создавать собственные типы столбцов, указывая шаблоны ячеек, используемые для отображения значений и редактирования. Задайте свойство CellTemplate, чтобы указать содержимое ячеек, которые отображают значения, но не позволяют редактировать. Задайте свойство CellEditingTemplate, чтобы указать содержимое ячеек в режиме редактирования. Если для свойства столбца IsReadOnly установлено значение true, значение свойства CellEditingTemplate никогда не используется.

Надеюсь, это даст вам лучшее представление о том, что происходит с вашим DataGrid

ИЗМЕНИТЬ

Что-то вроде этого позволит вам вручную добавить строку, когда вы нажмете "Enter" после выбора даты.

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;
            }
        }

Ответ 3

Если вам нужно решение для InvoiceDate, вот способ описания поведения для DateWorks, создав DataGridDateColumn следующим образом:

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;
    }
}

Затем я добавил еще две колонки в вашу сетку:

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

Если я сейчас кликнул в поле "Пользовательский", я получаю окно сообщений, выбираю дату и затем выкладываю, значение очищается, пока я не реализую INPC в 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));
    }

Теперь дата отображается в соответствии с набором DateFormatString.

Опять же, я знаю, что не отвечает на ваш первоначальный вопрос, но после моих поспешных комментариев ранее, я чувствовал себя обязанным, по крайней мере, придумать конкретное обходное решение.

Ответ 4

EDIT - Добавлен код, позволяющий сделать редактирование одним щелчком.

  • Изменено все привязки столбцов с помощью UpdateSourceTrigger=PropertyChanged - это потому, что значение по умолчанию LostFocus работает на уровне строки, а не на уровне ячеек, что означает, что вам нужно полностью оставить строку до того, как привязки вступят в силу. Это работает нормально для многих ситуаций, но не тогда, когда у вас есть два столбца, привязанных к одному и тому же свойству, потому что изменения, внесенные в один из этих столбцов, не отображаются сразу в другом столбце.
  • Задайте IsHitTestVisible="False" шаблону без редактирования центрального столбца. Мой первый подход состоял в том, чтобы сделать столбец только для чтения и использовать только CellTemplate... Но это не вызвало событие AddingNewItem. Кажется, вам НЕОБХОДИМО измениться с обычной ячейки на ячейку редактирования для этого события, чтобы запустить ее, но поскольку ваш шаблон без редактирования не является тем, с которым вы хотите взаимодействовать, отключить тестирование ударов имеет смысл. Таким образом, вы вынуждаете пользователя перейти в режим редактирования, а затем запускать событие, прежде чем сможете вводить ввод.
  • Обработано событие CurrentCellChanged DataGrid. В обработчике используйте методы CommitEdit(), чтобы убедиться, что ранее выбранная ячейка покидает режим редактирования и асинхронный вызов BeginEdit(), чтобы сразу же отредактировать текущую ячейку, не дожидаясь второго щелчка.
  • Обработано событие Loaded DatePickers внутри CellEditingTemplates. В обработчике, используя Keyboard.Focus(), чтобы сфокусироваться на DatePicker, как только он будет загружен, сохранение пользователю необходимости щелкнуть в третий раз, чтобы сосредоточить внимание на элементе управления.

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-за:

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);
}

Ответ 5

Первая часть кода - это только показать дату в "Рабочий столбец". Чтобы исправить щелчок дважды для редактирования, вы можете использовать вспомогательный класс.

Надеюсь, что это поможет...

<Window x:Class="WpfApplicationAnswerForStackOverflow.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>

Исправление для редактирования одного клика:

Использование:

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

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

Класс

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
    }
}