WPF динамически привязывается к DataGridTemplateColumn

Я пишу свой первый проект в WPF и не могу осмыслить остальную проблему.

У меня есть DataGrid, который использует ItemSource из таблицы DataSet (локальная база данных в XML). Пользователь должен иметь возможность добавлять столбцы в DataSet / DataGrid и устанавливать столбец DataTemplate, например текст, изображение, дату, ...

Поэтому я должен использовать один DataTemplate для нескольких столбцов и изменить путь привязки на основе имени столбца, например:

  <DataTemplate x:Key="ImageColumnTemplate">
        <Grid>
            <Image Source="{Binding Path=CURRENT_COLUMN_NAME Converter={StaticResource ImageReader}}" />
            <TextBox Text="{Binding Path=CURRENT_COLUMN_NAME}"/>
        </Grid>
    </DataTemplate>

Я понимаю, что это неправильный подход, но мне не удалось найти решение, которое:

-Не основан на сериализации / клонировании XAML - не работает, потому что теряются родительские ссылки.

-Возможность записывать значение в строку в отличие от "Path =." используя унаследованный DataGridBoundColumn вместо DataGridTemplateColumn.

DataGridTextColumn делает это каким-то образом, и он работает:

 Dim fGridCol = New DataGridTextColumn() With {.Header = fColumn.ColumnName}
 fGridCol.Binding = New Binding(fColumn.ColumnName) With {.Mode = BindingMode.TwoWay}

Но DataGridTemplateColumn не имеет привязки, а DataGridBoundColumn не записывает значение, если оно унаследовано.

Как вы можете заставить эту работу работать?

ИЗМЕНИТЬ

Позвольте мне поставить свой вопрос в ином контексте:

Лучшее, что у меня есть на данный момент:

<Window x:Class="MainWindow"
    ...
    <Window.Resources>

        <local:CellStringReader x:Key="StringReader" />
        <local:CellImageReader x:Key="ImageReader" />

        <Style x:Key="TextBlockToggle" TargetType="{x:Type TextBlock}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridCell}}, Path=IsEditing}" Value="True">
                    <Setter Property="Visibility" Value="Hidden"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>

        <Style x:Key="TextBoxToggle" TargetType="{x:Type TextBox}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=DataGridCell}, Path=IsEditing}" Value="False">
                    <Setter Property="Visibility" Value="Hidden"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>

        <DataTemplate x:Key="ImageColumnTemplate">
            <Grid Focusable="True">

                <Grid HorizontalAlignment="Left"  Background="Transparent">
                    <Button PreviewMouseDown="SelectImageFile"  >
                        <Image x:Name="ImageTemplateImage" Height="20" Width="20"  
                        Source="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridCell}}, UpdateSourceTrigger=PropertyChanged , Converter={StaticResource ImageReader}}"/>
                    </Button>
                </Grid>

                <TextBlock x:Name="ImageTemplateTextBlock" Margin="25,0,0,0"
                Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridCell}}, UpdateSourceTrigger=PropertyChanged , Converter={StaticResource StringReader}}"/>

                <TextBox x:Name="ImageTemplateTextBox" Margin="23,0,0,0" BorderThickness="0" Style="{StaticResource TextBoxToggle}" 
                         Text="{Binding Mode=TwoWay, Path=., RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridCell}}, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource StringReader}}"/>

            </Grid>
        </DataTemplate>

    </Window.Resources>

    <Grid>
        ...

        <DataGrid x:Name="LocalGrid" Grid.Row="1"  AutoGenerateColumns="False" CanUserAddRows="False">
            <DataGrid.RowValidationRules>
                <local:RowDataValidationRule/>
            </DataGrid.RowValidationRules>
        </DataGrid>

        ...
    </Grid>
</Window>

И

    Class MainWindow

        Protected Overrides Sub OnInitialized(e As EventArgs)
            LocalGrid.ItemsSource = Base.Tables("Local").DefaultView
            CreateColumns()
        End Sub

        Private WithEvents Base As New Base
        Private WithEvents LocalTable As DataView = Base.Tables("Local").DefaultView

        Private Sub CreateColumns()
            Dim LocalTable = Base.Tables("Local")
            Dim TypesTable = Base.Tables("ColumnTypes")

            For Each fColumn As DataColumn In LocalTable.Columns

                Dim ColumnType As String = (From fRow As DataRowView In TypesTable.DefaultView Where fRow.Item("Name") = String.Format("Local." & fColumn.ColumnName) Select fRow.Item("Template") Take 1).FirstOrDefault()

                If ColumnType = "Image" Then 'THIS IS IMAGE COLUMN
                    Dim ImageColumn As New DataGridTemplateColumn With {.Header = fColumn.ColumnName}
                    ImageColumn.CellTemplate = Me.FindResource("ImageColumnTemplate")
                    ImageColumn.CellEditingTemplate = Me.FindResource("ImageColumnTemplate")
                    LocalGrid.Columns.Add(ImageColumn)
                Else 'THIS IS REGILAR COLUMN
                    Dim fGridCol = New DataGridTextColumn() With {.Header = fColumn.ColumnName}
                    fGridCol.Binding = New Binding(fColumn.ColumnName) With {.Mode = BindingMode.TwoWay, .UpdateSourceTrigger = UpdateSourceTrigger.LostFocus}
                    LocalGrid.Columns.Add(fGridCol)
                End If

            Next
        End Sub

        Private Sub SelectImageFile(ByVal sender As Object, ByVal e As RoutedEventArgs)
            'This creates OpenFileDialog on button click
        End Sub

    End Class

    Public Class CellStringReader : Implements IValueConverter
        Private EditingCell As DataGridCell

        Public Overridable Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
            Dim Cell As DataGridCell = value
            Dim Row As DataRowView = Cell.DataContext
            Dim Column As DataGridColumn = Cell.Column

            If Cell.IsEditing Then
                EditingCell = Cell
            Else
                EditingCell = Nothing
            End If

            Return Row.Item(Column.Header)
        End Function

        Public Overridable Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
            If EditingCell Is Nothing Then 'This is not callded, ever.
                Throw New Exception("No cell editing")
            End If
            Return EditingCell
        End Function
    End Class

    Public Class CellImageReader : Inherits CellStringReader

        Public Overrides Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object
            value = MyBase.Convert(value, targetType, parameter, culture)

            If IsDBNull(value) OrElse String.IsNullOrWhiteSpace(value) Then
                Return Nothing
            ElseIf IO.File.Exists(value) Then
                Return New BitmapImage(New Uri(value))
            End If
        End Function

        Public Overrides Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object
            Throw New NotSupportedException
        End Function

    End Class

Вот как это выглядит

Проблема в том, что редактирование TextBox в сгенерированном столбце Image не вызывает CellStringReader.ConvertBack () и не записывает измененное значение базового DataRow.

Я так понимаю, это потому, что "Путь =." в TextBox Binding, но я не знаю альтернатив.

При синтаксическом анализе XAML в строке Button PreviewMouseDown прерывается из-за отсутствия контекста, и он все равно не записывает значение.

Мой вопрос в том, как заставить TextBox записывать новое значение в DataRow.?

Надеюсь, теперь будет больше сеансов, и извините за длинный пост.


wpf
person Dreigo Undead    schedule 08.12.2018    source источник
comment
Столбец шаблона может иметь любое количество свойств, к которым он привязан. Он принципиально отличается от текстового столбца. Если вы динамически сгенерировали этот столбец, вы можете просто привязать источник изображения к имени столбца и текст текстового поля к имени столбца.   -  person Andy    schedule 08.12.2018
comment
Если вы используете mvvm и привязываетесь к командам, то это позднее обнаружение. Когда вы выполняете xamlreader.parse, при создании кнопки с привязкой к команде не будет ошибки.   -  person Andy    schedule 08.12.2018
comment
Если я понимаю, что вы имеете в виду, как вы можете это сделать? ‹TextBox Text = {Binding Path = ColumnName} /› не будет работать, ни Path = Item [ColumnName]. Cud, напишите, пожалуйста, какой-нибудь пример?   -  person Dreigo Undead    schedule 08.12.2018
comment
Если у вас есть столбец с именем Name, вы можете привязать его: ‹DataGrid Name = dg AutoGenerateColumns = False› ‹DataGrid.Columns› ‹DataGridTemplateColumn Header = Name› ‹DataGridTemplateColumn.CellTemplate› ‹DataTemplate› TextBox Text = {‹Binding Name} / ›‹/DataTemplate›‹ /DataGridTemplateColumn.CellTemplate ›‹/DataGridTemplateColumn›‹ /DataGrid.Columns ›‹/DataGrid›   -  person Andy    schedule 08.12.2018
comment
Хорошо, мне удалось сломать ногу и заставить ее работать, проанализировав строку с помощью XamlReader, теперь я получаю эту проблему с кнопкой PreviewMouseDown и кнопку исключения, нажав кнопку: ArgumentException: невозможно привязать к целевому методу, потому что его подпись или прозрачность безопасности несовместимы с подписью тип делегата. Любые идеи? PS может мне стоит сделать из этого новый вопрос?   -  person Dreigo Undead    schedule 09.12.2018
comment
Либо ваша подпись обработчика действительно неверна, либо вы не предоставили объект для поиска обработчика. Думаю, я уже рекомендовал это, но вместо этого вам следует привязать команду. Привязки разрешаются поздно, поэтому, когда вы xamlreader.parse, он вылетает и сгорает, если не может найти команду.   -  person Andy    schedule 09.12.2018
comment
Спасибо за помощь, Энди, ценю это!   -  person Dreigo Undead    schedule 09.12.2018


Ответы (2)


Я действительно не понимаю некоторых из ваших объяснений.

Я бы, вероятно, подхожу к этому, создавая xaml в виде строк для каждой опции. Пользователь выбирает, какой из них он хочет использовать. Манипулируйте строками и замените название свойства на заполнитель. Затем xamlreader.parse строки в столбец базы данных, который вы затем добавляете в коллекцию столбцов вашей сетки данных. Вот образец, который даст вам представление о подходе:

https://gallery.technet.microsoft.com/WPF-Dynamic-XAML-Awkward-41b0689f

В нем есть два файла .txt с некомпилированным "плоским" контуром xaml. Он обрабатывает их как xml. Образец строит всю сетку данных, но у вас может быть сетка данных на том месте, с которого вы начинаете.

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        // Get the datagrid shell
        XElement xdg = GetXElement(@"pack://application:,,,/dg.txt");  
        XElement cols = xdg.Descendants().First();     // Column list
        // Get the column template
        XElement col = GetXElement(@"pack://application:,,,/col.txt");  

        DateTime mnth = DateTime.Now.AddMonths(-6);

        for (int i = 0; i < 6; i++)
        {
            DateTime dat = mnth.AddMonths(i);
            XElement el = new XElement(col);
            // Month in mmm format in header
            var mnthEl = el.Descendants("TextBlock")
                        .Single(x => x.Attribute("Text").Value.ToString() == "xxMMMxx");
            mnthEl.SetAttributeValue("Text", dat.ToString("MMM"));

            string monthNo = dat.AddMonths(-1).Month.ToString();
            // Month as index for the product
            var prodEl = el.Descendants("TextBlock")
                        .Single(x => x.Attribute("Text").Value == "{Binding MonthTotals[xxNumxx].Products}");
            prodEl.SetAttributeValue("Text",
                "{Binding MonthTotals[" + monthNo + "].Products}");
            // Month as index for the total
            var prodTot = el.Descendants("TextBlock")
                        .Single(x => x.Attribute("Text").Value == "{Binding MonthTotals[xxNumxx].Total}");
            prodTot.SetAttributeValue("Text",
                "{Binding MonthTotals[" + monthNo + "].Total}");
            cols.Add(el);
        }

        string dgString = xdg.ToString();
        ParserContext context = new ParserContext();
        context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
        context.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
        DataGrid dg = (DataGrid)XamlReader.Parse(dgString, context);
        Root.Children.Add(dg);
    }
    private XElement GetXElement(string uri)
    {
        XDocument xmlDoc = new XDocument();
        var xmltxt = Application.GetContentStream(new Uri(uri));
        string elfull = new StreamReader(xmltxt.Stream).ReadToEnd();
        xmlDoc = XDocument.Parse(elfull);
        return xmlDoc.Root;
    }

В качестве альтернативы вы можете использовать string.replace. Или оба.

person Andy    schedule 08.12.2018
comment
Спасибо за ответ, но я не могу сделать сгенерированные столбцы доступными для записи. Я изменил TextBlock на TextBox в col.txt DataTemplate и сделал DataGrid не только для чтения, и ничего не сработало. Значения в SalesMan.MonthTotals не изменились. Есть какие-нибудь горячие идеи, чтобы это исправить? - person Dreigo Undead; 08.12.2018
comment
Вы изменили код, который ожидает найти TextBlock, на TextBox? var prodEl = el.Descendants (TextBlock) - person Andy; 08.12.2018
comment
Да, конечно. У меня нет исключений, и значения отображаются правильно. Проблема только в том, что при изменении значения в TextBox изменение не записывается в исходное свойство / объект. - person Dreigo Undead; 08.12.2018
comment
Я не понимаю, почему это имеет какое-либо отношение к вашему вопросу. Он не обновляется, потому что привязка использует индекс для коллекции внутри связанной коллекции. У каждого продавца есть список общих продаж с записью за месяц. Сетка данных предназначена только для чтения, а цифры предназначены только для чтения. Это итоги, которые будут получены от продаж. - person Andy; 08.12.2018
comment
Извините, что сбил вас с толку, я отредактировал исходный вопрос, теперь по нему будет больше сеанса. Пожалуйста, посмотрите. - person Dreigo Undead; 08.12.2018

Кровавая победа!

Итак, необходимая функциональность:

  • Использование одного DataTemlate для нескольких столбцов в DataGrid
  • DataTemplate требует привязки TwoWay и должен иметь возможность записи в базовый объект DataRow
  • Вторичная строка редактирования с OpenFileDialog

XAML:

<Window x:Class="MainWindow"
    ...

    <Window.Resources>
        <local:ImageReader x:Key="ImageReader" />
        ...

        <DataTemplate x:Key="ImageColumnReadTemplate">
            <Grid>
                <Grid HorizontalAlignment="Left"  Background="Transparent">
                    <Button IsEnabled="False"  >
                        <Image x:Name="ImageTemplateImage" Height="18" Width="18"  Source="{Binding Path=COLUMN_NAME, Converter={StaticResource ImageReader}}" />
                    </Button>
                </Grid>
                <TextBlock x:Name="ImageTemplateTextBlock" Margin="25,0,0,0" Text="{Binding Path=COLUMN_NAME}"/>
            </Grid>
        </DataTemplate>

        <DataTemplate x:Key="ImageColumnWriteTemplate">
            <Grid>
                <Grid HorizontalAlignment="Left" Background="Transparent">
                    <Button Command="{Binding ClickCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" >
                        <Image x:Name="ImageTemplateImage" Height="18" Width="18"  Source="{Binding Path=COLUMN_NAME, Converter={StaticResource ImageReader}}" />
                    </Button>
                </Grid>
                <TextBox x:Name="ImageTemplateTextBox" Margin="23,0,0,0" BorderThickness="0" Text="{Binding Path=COLUMN_NAME}"/>
            </Grid>
        </DataTemplate>

    </Window.Resources>

    <Grid>
        ...
        <DataGrid x:Name="LocalGrid" Grid.Row="1"  AutoGenerateColumns="False" CanUserAddRows="False">
            ...
        </DataGrid>
        ...
    </Grid>
</Window>

Код XAML DataTemplate читается XmlWriter, изменяет привязки и создает новый экземпляр DataTemplate с помощью XDocument.Parse ():

For Each fColumn As DataColumn In LocalTable.Columns
    Dim ImageColumn As New DataGridTemplateColumn With {.Header = fColumn.ColumnName}
    ImageColumn.CellTemplate = CreateTemplate("ImageColumnReadTemplate", fColumn.ColumnName)
    ImageColumn.CellEditingTemplate = CreateTemplate("ImageColumnWriteTemplate", fColumn.ColumnName)
    LocalGrid.Columns.Add(ImageColumn)
Next

...

Private Function CreateTemplate(TemplateName As String, ColumnName As String) As DataTemplate
    Dim Template As DataTemplate = Me.FindResource(TemplateName)

    Dim StrBuilder = New StringBuilder()
    Dim Settings = New XmlWriterSettings() With {.Indent = True, .OmitXmlDeclaration = True}
    Dim dsm = New XamlDesignerSerializationManager(XmlWriter.Create(StrBuilder, Settings)) With {.XamlWriterMode = XamlWriterMode.Expression}
    XamlWriter.Save(Template, dsm)

    StrBuilder = StrBuilder.Replace("COLUMN_NAME", ColumnName)

    Dim xmlDoc = XDocument.Parse(StrBuilder.ToString())
    'IO.File.WriteAllLines("D:\xml.txt", xmlDoc.ToString.Split(vbNewLine)) 'Debug

    Dim NewTemplate As DataTemplate = XamlReader.Parse(xmlDoc.ToString())

    Return NewTemplate
End Function

XamlWriter имеет несколько ограничений, одно из них - запись / экспорт привязок, которые игнорируются. Вот как заставить его писать привязки: [источник] (https://www.codeproject.com/Articles/27158/XamlWriter-and-Bindings-Serialization)

Imports System.ComponentModel
Imports System.Windows.Markup

Class BindingConvertor
    Inherits ExpressionConverter

    Public Overrides Function CanConvertTo(ByVal context As ITypeDescriptorContext, ByVal destinationType As Type) As Boolean
        If destinationType = GetType(MarkupExtension) Then
            Return True
        Else
            Return False
        End If
    End Function

    Public Overrides Function ConvertTo(ByVal context As ITypeDescriptorContext, ByVal culture As System.Globalization.CultureInfo, ByVal value As Object, ByVal destinationType As Type) As Object
        If destinationType = GetType(MarkupExtension) Then
            Dim bindingExpression As BindingExpression = TryCast(value, BindingExpression)
            If bindingExpression Is Nothing Then Throw New Exception()
            Return bindingExpression.ParentBinding
        End If

        Return MyBase.ConvertTo(context, culture, value, destinationType)
    End Function
End Class

Module EditorHelper

    Sub RegisterBindingConvertor
        EditorHelper.Register(Of BindingExpression, BindingConvertor)()
    End Sub

    Sub Register(Of T, TC)()
        Dim attr As Attribute() = New Attribute(0) {}
        Dim vConv As TypeConverterAttribute = New TypeConverterAttribute(GetType(TC))
        attr(0) = vConv
        TypeDescriptor.AddAttributes(GetType(T), attr)
    End Sub
End Module

...

Class MainWindow
    Public Sub New()
        EditorHelper.RegisterBindingConvertor()
        '...
    End Sub
    '...
End Class

Для дескриптора кнопки PreviewMouseClick не работает из-за другого ограничения XmlWriter. Привязка к Buttom.Command действительно работала:

Public ReadOnly Property ClickCommand As ICommand = New CommandHandler(AddressOf SelectImageFile, True)

Public Class CommandHandler
    Implements ICommand

    Private _action As Action
    Private _canExecute As Boolean

    Public Sub New(ByVal action As Action, ByVal canExecute As Boolean)
        _action = action
        _canExecute = canExecute
    End Sub

    Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
        _action()
    End Sub
    Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
        Return _canExecute
    End Function

    Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
End Class

Public Sub SelectImageFile()
    If LocalGrid.CurrentColumn Is Nothing Then Exit Sub

    Dim fd As OpenFileDialog = New OpenFileDialog()
    fd.ShowDialog()
    Dim Row As DataRowView = LocalGrid.CurrentItem
    Row.Item(LocalGrid.CurrentColumn.Header) = fd.FileName

    LocalGrid.CommitEdit()
    BaseGrid_RowEditEnding()
End Sub

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

person Dreigo Undead    schedule 09.12.2018