Reputation: 39
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
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.
Upvotes: 0
Views: 1421
Reputation: 39
Bloody victory!
So to summary, required functionality is:
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.
Upvotes: 0
Reputation: 12276
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.
Upvotes: 1