Is it possible to create a template for a GridViewColumn, without fixed data binding?
Asked Answered
D

1

3

Is this possible?

I have a ListView and I want to create a template for the columns, so that each column that I mark as a 'cellTextBox' displays with a textbox in it (and also calls the TextBox_LostFocus() on the LostFocus event). I'd really like to use a single template rather than defining a DockPanel and TextBox for every single column. However, each column is going to be bound to a different column in the data source.

In other words, I'd like a "cookie cutter" for GridViewColumns that allows me to specify a different binding path for each one.

I tried something like this (among other things), but it doesn't work.

Any ideas?

<Page.Resources>
            <DataTemplate x:Key="cellTextBox">
                <DockPanel>
                <TextBox
                        LostFocus="TextBox_LostFocus"
                        Width="100"
                        Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=DisplayMemberBinding}"
                />
                </DockPanel>
            </DataTemplate>
</Page.Resources>
<StackPanel>

        <ListView ScrollViewer.HorizontalScrollBarVisibility="Disabled" 
                  HorizontalAlignment="Stretch" 
                  ItemsSource="{Binding Tables[0]}" 
                  Width="Auto"
                  x:Name="Service_Tasks">

            <ListView.View>
                <GridView>
                    <GridViewColumn Width="120" CellTemplate="{StaticResource cellTextBox}" DisplayMemberBinding={Binding Path=Field1} Header="Field1" />
                    <GridViewColumn Width="120" CellTemplate="{StaticResource cellTextBox}" DisplayMemberBinding={Binding Path=Field2} Header="Field2" />
                    <GridViewColumn Width="120" CellTemplate="{StaticResource cellTextBox}" DisplayMemberBinding={Binding Path=Field3} Header="Field3" />
                    <GridViewColumn Width="120" CellTemplate="{StaticResource cellTextBox}" DisplayMemberBinding={Binding Path=FieldN} Header="FieldN" />
                    <!-- ... other columns ... -->
                </GridView>
            </ListView.View>

        </ListView>
</StackPanel>

EDIT - I thought I would share the Visual Basic translation of @H.B.'s solution, which worked for me. Perhaps this will help out someone else.

Imports System.Windows.Markup

Public Class TemplateBuilderExtension : Inherits StaticExtension
    Dim _path As String
    Shared _tagpath As String

    'Private TagPath As String

    Shared Property TagPath As String
        Get
            TagPath = _tagpath
        End Get
        Private Set(ByVal value As String)
            _tagpath = value
        End Set
    End Property

    Property Path As String
        Get
            Path = _path
        End Get
        Set(ByVal value As String)
            _path = value
        End Set
    End Property

    Public Overrides Function ProvideValue(ByVal serviceProvider As System.IServiceProvider) As Object
        TagPath = Path
        Dim resourceExt = New StaticResourceExtension("TemplateBuilder_BaseTemplate")
        'Dim baseTemplate As DataTemplate = DirectCast(resourceExt.ProvideValue(serviceProvider), DataTemplate)
        ProvideValue = DirectCast(resourceExt.ProvideValue(serviceProvider), DataTemplate)
    End Function

    Public Sub New()
        MyBase.new()
    End Sub

    Public Sub New(ByVal Path As String)
        Me.Path = Path
    End Sub
End Class

Public Class TemplateBuilderTagExtension : Inherits MarkupExtension
    Public Sub New()
        MyBase.New()
    End Sub
    Public Overrides Function ProvideValue(ByVal serviceProvider As System.IServiceProvider) As Object
        ProvideValue = New Binding(TemplateBuilderExtension.TagPath)
    End Function
End Class

Obviously, the XAML is the same, regardless.

Do not forget to add the following namespace reference to your root-level tag:

xmlns:me="clr-namespace:WpfApplication1", replacing WpfApplication with whatever your application's root namespace is (this can be found in your application's properties).

Thanks again @H.B.!

Disbelieve answered 24/8, 2011 at 0:20 Comment(0)
K
4

The only method i can think of right now to inline this is using markup-extensions as you try to pass more than one value to one property.

Getting variable data into the template seems to be non-trivial though and the following approach using a static property is quite the hack, maybe you can think of something better:

<ListView ItemsSource="{Binding Data}">
    <ListView.Resources>
        <!-- x:Shared="False" because otherwise the Tag extension is only evaluated the first time the resource is accessed -->
        <DataTemplate x:Shared="false" x:Key="TemplateBuilder_BaseTemplate">
            <TextBox Text="{me:TemplateBuilderTag}" Width="100" LostFocus="TextBox_LostFocus" />
        </DataTemplate>
    </ListView.Resources>
    <ListView.View>
        <GridView>
            <GridViewColumn CellTemplate="{me:TemplateBuilder Name}"  />
            <GridViewColumn CellTemplate="{me:TemplateBuilder Occupation}" />
        </GridView>
    </ListView.View>
</ListView>
public class TemplateBuilderExtension : MarkupExtension
{
    public string Path { get; set; }

    public TemplateBuilderExtension() { }
    public TemplateBuilderExtension(string path)
    {
        Path = path;
    }

    // Here be dirty hack.
    internal static string TagPath { get; private set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        TagPath = Path;
        var resourceExt = new StaticResourceExtension("TemplateBuilder_BaseTemplate");
        // This line causes the evaluation of the Tag as the resource is loaded.
        var baseTemplate = resourceExt.ProvideValue(serviceProvider) as DataTemplate;
        return baseTemplate;
    }
}

public class TemplateBuilderTagExtension : MarkupExtension
{
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return new Binding(TemplateBuilderExtension.TagPath);
    }
}

I tried to use a custom service provider when getting the resource but unfortunately it did not arrive in ProvideValue of the tag, that would have been a better way to get the path accross.

You could of course create the template dynamically from scratch in the TemplateBuilderExtension, that way you will not run into such issues.

Katydid answered 24/8, 2011 at 4:42 Comment(5)
+1 this looks really promising. I will try it this evening since I dont have my code with me at the moment. I'm a little surprised that XAML offers ways to template controls, but not elements like GridViewColumn which are not Visual elements but seem like they would be used frequently. Am I missing something obvious? Should I be using something besides a ListView for these types of lists (keeping in mind that this project is in 3.5)?Disbelieve
@transistor1: Well, the ListView is a view, it probably is not meant for editing, you could use the DataGrid from the WPF Toolkit instead.Katydid
Quick question: does the line public string Path { get; set; } mean that the Path property is not stored internally (i.e. get and set are not implemented)? Or is it a default property implementation? I'm more accustomed to VB, though I've done some rudimentary C# coding, so I'm translating it.Disbelieve
... pretty sure it is a default property implementation.Disbelieve
@transistor1: That's called auto-implemented property, just shorthand for a private backing field and a public property which exposes it. Of course the field cannot be accessed directly that way...Katydid

© 2022 - 2024 — McMap. All rights reserved.