【问题标题】:WPF Bound dynamically to DataGridTemplateColumnWPF 动态绑定到 DataGridTemplateColumn
【发布时间】:2019-05-09 21:31:07
【问题描述】:

在 WPF 中编写我的第一个项目,我无法解决闲散问题。

我有一个使用 DataSet 表中的 ItemSource 的 DataGrid(XML 中的本地 DB) 用户必须能够将列添加到 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

问题是,在生成的 Image 列中编辑 TextBox 不会调用 CellStringReader.ConvertBack() 并且不会写入底层 DataRow 的更改值。

我知道这是因为“Path=。”在 TextBox Binding 中,但我不知道任何替代方案。

在字符串中解析 XAML 会中断 Button PreviewMouseDown,因为缺少上下文,并且无论如何它都不会写入值。

我的问题是如何让 TextBox 在 DataRow 中写入新值?

希望现在有更多的降神会,很抱歉发了很长的帖子。

【问题讨论】:

  • 一个模板列可以有任意数量的属性绑定到它里面的东西。它与文本列根本不同。如果您动态生成该列,则可以将图像的源绑定到列名,将文本框的文本绑定到列名。
  • 如果您使用 mvvm 并绑定到命令,那么这是后期发现。当您使用 xamlreader.parse 创建一个绑定到命令的按钮时,它不会出错。
  • 如果我明白你的意思,你怎么能这样做? 不起作用,Path=Item[ColumnName] 也不起作用。倒是你写个例子好吗?
  • 如果你有一个名为 Name 的列,那么你可以绑定它:
  • 好的,我设法打破了我的腿并通过使用 XamlReader 解析字符串使其工作,现在我得到了按钮 PreviewMouseDown 问题和期望单击按钮:“ArgumentException:无法绑定到目标方法,因为它的签名或安全透明度与委托类型不兼容。”有任何想法吗? PS也许我应该对此提出新问题?

标签: wpf


【解决方案1】:

我并没有真正理解你的一些解释。

我可能会采用的方法是将 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。 或两者兼而有之。

【讨论】:

  • 感谢您的回答,但我无法使生成的列可写。我在 col.txt DataTemplate 中将 TextBlock 更改为 TextBox,并使 DataGrid 不是只读的并且没有任何效果。 SalesMan.MonthTotals 中的值没有改变。有什么想法可以解决这个问题吗?
  • 您是否将期望找到 TextBlock 的代码更改为 TextBox? var prodEl = el.Descendants("TextBlock")
  • 是的,当然。我没有例外,并且值显示正确。唯一的问题是在 TextBox 中更改值时,更改不会写入源属性/对象中。
  • 我不明白为什么这与您的问题有关。它没有更新,因为绑定正在使用绑定集合中的集合上的索引。每个推销员都有一个总销售额列表,每个月都有一个条目。数据网格是只读的,并且这些数字用于只读目的。它们是销售总额。
  • 抱歉让您感到困惑,我编辑了原始问题,现在应该进行更多的降神会。请看一看。
【解决方案2】:

血腥的胜利!

总而言之,所需的功能是:

  • 对 DataGrid 中的多个列使用单个 DataTemplate
  • 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>

XmlWriter 读取 XAML DataTemplate 代码,修改绑定并使用 XDocument.Parse() 创建 DataTemplate 的新实例:

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

由于另一个 XmlWriter 限制,按钮句柄 PreviewMouseClick 不起作用。绑定到 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

这可行,但欢迎提出如何缩短此时间的任何建议。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-10-13
    • 2011-02-06
    • 2012-04-21
    • 2011-04-06
    • 2011-01-20
    • 2018-02-11
    • 2011-10-28
    • 2017-11-22
    相关资源
    最近更新 更多