0
votes

Writing my first project in WPF and cannot wrap my mind around fallowing problem.

I have a DataGrid that uses ItemSource from DataSet Table (local DB in XML) User must be able to add columns to DataSet/DataGrid and set column DataTemplate, like text, image, date, ...

So I must use single DataTemplate for multiple columns, and change binding path based on column name, like:

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

I understand that this approach is not correct, but I failed to find solution that:

-Is not XAML serialization / cloning based - does not work because loses parent references.

-Able to write value to row unlike "Path=." using inherited DataGridBoundColumn instead of DataGridTemplateColumn.

DataGridTextColumn does this is some way, and it works:

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

But DataGridTemplateColumn has no bind, and DataGridBoundColumn does not write value if inherited.

How can you do make this work?

EDIT

Allow me to put my question in different context:

The best I have got so far:

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

And

    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

This is how it looks

The problem is, that editing TextBox in generated Image column does not call CellStringReader.ConvertBack() and does not write changed value of underlying DataRow.

I understand that this is because "Path=." in TextBox Binding, but I don't know any alternatives.

Parsing XAML in string breaks Button PreviewMouseDown, because of missing context, and it does not write value anyway.

My question is how to make TextBox write new value in DataRow.?

Hope it makes more seance now & sorry for long post.

2
A templatecolumn can have any number of properties the stuff in it binds to. It is fundamentally different from a text column. If you dynamically generated that column you could just bind source of the image to the column name and text of the textbox to column name.Andy
If you use mvvm and bind to commands then this is late discovery. When you xamlreader.parse it won't error when you create a button with a binding to a command.Andy
If I understand what you mean, how can you do that? <TextBox Text="{Binding Path=ColumnName}"/> will not work, nor Path=Item[ColumnName]. Cud you write some example please?Dreigo Undead
If you have a column called Name then you can bind that: <DataGrid Name="dg" AutoGenerateColumns="False"> <DataGrid.Columns> <DataGridTemplateColumn Header="Name"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <TextBox Text="{Binding Name}"/> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid>Andy
Okay, I managed to break my leg and make it work by parsing string with XamlReader, now I get that Button PreviewMouseDown problem and expection clicking button: "ArgumentException: Cannot bind to the target method because its signature or security transparency is not compatible with that of the delegate type." Any ideas? PS maybe I should make new question out of this?Dreigo Undead

2 Answers

1
votes

I don't really follow some of your explanation.

The way I would probably approach this is to build xaml as strings for each option. The user picks which one they want to use. Manipulate the strings and substitute property name for a place holder. Then xamlreader.parse the string into a databasecolumn which you then add to the columns collection of your datagrid. There's a sample will give you a flavour of the approach:

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

That has two .txt files in it which have the uncompiled "flat" outline xaml. It processes these as xml. The sample is building the entire datagrid but you could have a datagrid in place you start with.

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

You could alternatively use string.replace. Or both.

0
votes

Bloody victory!

So to summary, required functionality is:

  • Using single DataTemlate to multiple columns in DataGrid
  • DataTemplate requires TwoWay binding and must be able to write in underlying DataRow object
  • Secondary edit row with 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 code is read by XmlWriter, modified bindings and created new instance of DataTemplate using 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 has multiple limitations, one of them is writing/exporting bindings, that are ignored. This is how to make it write bindings: [source] (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

For the button handle PreviewMouseClick does not work because of another XmlWriter limitation. Binding to Buttom.Command did work:

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

This works, but any recommendations how to make this shorter are welcome.