1

I have created a simple WPF application using XAML and PowerShell, consisting of a TabControl in whose child TabItems I wish to display multiple kinds of data. Depending on the type of data provided, I want the TabControl's child TabItems to use a different DataTemplate.

I understand that the best (only?) way to do this in my situation is to create a custom DataTemplateSelector class in C# to handle the template selection.

I have attempted to do this, but am having difficulty with using my custom class. Here is the error I am getting:

Exception calling "Load" with "1" argument(s): "Cannot create unknown type
'{clr-namespace:myNamespace}myDataTemplateSelector'."
At line:101 char:1
+ $Window = [Windows.Markup.XamlReader]::Load($Reader)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : XamlParseException

I suspect I am improperly loading a required assembly or the namespace and thus unable to access my custom namespace and custom class. I have never used C# before, so I really appreciate any hand-holding offered.

Once I have resolved that problem, I know my C# custom class's internal logic will not work as desired, but that is a separate issue. The C# code appears to be valid, as I can run it independently and instantiate my custom class.

The XAML and code also works fine if I remove all of the DataTemplateSelector-related bits and add the following to the TabControl:

ContentTemplate="{StaticResource UserDataTemplate}"

Here is the code (including C#, XAML, PowerShell):

$Assemblies = @("System", "PresentationFramework", "WindowsBase", "System.Xaml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")

$cSharpSource = @"
using System;
using System.Windows;
using System.Windows.Controls;

namespace myNamespace
{
    public class myDataTemplateSelector : DataTemplateSelector
    {
        public DataTemplate UserDataTemplate
        { get; set; }

        public DataTemplate GroupDataTemplate
        { get; set; }

        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {   
            if (item as string == "User")
            {
                return UserDataTemplate;
            }
            else if (item as string == "Group")
            {
                return GroupDataTemplate;
            }
            else
            {
                return null;
            }
        }
    }
}
"@

Add-Type -TypeDefinition $cSharpSource -ReferencedAssemblies $Assemblies

Add-Type -AssemblyName PresentationFramework

[xml]$XAML = @"
<Window x:Name="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="650" Width="300" FontSize="11"
    xmlns:local="clr-namespace:myNamespace">

    <Window.Resources>
        <DataTemplate x:Key="HeaderTemplate">
            <Label Content="Header text" />
        </DataTemplate>

        <DataTemplate x:Key="UserDataTemplate">
            <Grid>
                <TextBlock Text="UserDataTemplate in use" />
            </Grid>                
        </DataTemplate>

        <DataTemplate x:Key="GroupDataTemplate">
            <Grid>
                <TextBlock Text="GroupDataTemplate in use" />
            </Grid>                
        </DataTemplate>
    </Window.Resources>

    <StackPanel>

        <Button x:Name="UserTabItem_Button" Content="Load UserTabItem" />

        <Button x:Name="GroupTabItem_Button" Content="Load GroupTabItem" />

        <TabControl x:Name="TabControl" ItemTemplate="{StaticResource HeaderTemplate}">

            <TabControl.ContentTemplateSelector>
                <local:myDataTemplateSelector 
                    UserDataTemplate="{StaticResource UserDataTemplate}"
                    GroupDataTemplate="{StaticResource GroupDataTemplate}"/>
            </TabControl.ContentTemplateSelector>

        </TabControl>

    </StackPanel>
</Window>
"@


# Parse the XAML
$Reader = (New-Object System.Xml.XmlNodeReader $XAML)
$Window = [Windows.Markup.XamlReader]::Load($Reader)

# Iterate through each XAML node and create a variable for each node
$XAML.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object {
    New-Variable -Name $_.Name -Value $Window.FindName($_.Name) -Force
}

# Example data
$UserTabItem = [PSCustomObject]@{
    'ObjectClass' = 'User'
}

$GroupTabItem = [PSCustomObject]@{
    'ObjectClass' = 'Group'
}

# Clicks to add Child TabItems to TabControl
$UserTabItem_Button.Add_Click({
    $TabControl.AddChild($UserTabItem)
})

$GroupTabItem_Button.Add_Click({
    $TabControl.AddChild($GroupTabItem)
})

$Window.ShowDialog()

I have also explored storing XAML DataTemplates as PowerShell variables and setting the TabControl's ContentTemplate property to the appropriate DataTemplate before adding the Child. This was unsuccessful and perhaps not possible after reading about WPF's Templating documentation.

I am open to other approaches. Thanks for your time.

1
  • 1
    I can't spot any obvious errors, but I have very limited PS experience. with that said, this implementation seems like a complete torture to me. think of it like notepad vs ps-ISE. why don't you first develop this in WPF Visual studio project where you get full compiler and runtime support and other very useful tools. once you got it working there, you can convert it to power shell if you must. but I'd actually just build a WPF control library and consume it from PS. feel free to send me an email to eshyah at yahoo com. Commented Aug 26, 2018 at 5:43

3 Answers 3

2

As Slime recipe suggested, creating a control library in Visual Studio first was far less painful.

I created a new 'Class Library (.NET Framework)' project in Visual Studio, pasted existing syntactically-valid C# code into the solution, added references to the appropriate assemblies, and built the project.

I copied the resultant myDataTemplateSelector.dll file to the same directory as the PowerShell script file.

I loaded a fresh PowerShell console (re-using a console did not load the assembly correctly) and ran the following commands to test the DLL:

Add-Type -Path .\myDataTemplateSelector.dll
[myNamespace.myDataTemplateSelector]::New()

This successfully instantiated my custom class.

Finally, I updated my XAML:

xmlns:local="clr-namespace:myNamespace;assembly=myDataTemplateSelectorLibrary"

The WPF app now runs!

I would appreciate any other answers that can explain how to accomplish the same thing without having to compile the C# code in Visual Studio, as I would rather not rely on a non-human-readable file (i.e. a DLL file) in this project.

Edit - fully answered:

Slice recipe's suggestion to programmatically find the assembly name instead of relying on what I assumed it would be (based on my C# code) led me down the correct path. Thanks again.

When running C# code in PowerShell, one typically uses Add-Type, which loads the code into memory only.

If you specify source code, Add-Type compiles the specified source code and generates an in-memory assembly that contains the new .NET Framework types.

https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/add-type?view=powershell-5.1

In order to access the metadata for the added code, one must use the -Passthru parameter of Add-Type:

-PassThru

Returns a System.Runtime object that represents the types that were added. By default, [Add-Type] does not generate any output.

I then stored the output of the revised Add-Type command:

$Type = Add-Type -TypeDefinition $cSharpSource -ReferencedAssemblies $Assemblies -PassThru

The assembly's full name then the be accessed:

> $Type.Assembly.Fullname
m0m5m4la, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null

The first string 'm0m5m4la' is the needed assembly name and appears to be randomly generated when the type is added, and acts as a reference to the assembly in memory.

Finally, it can be accessed as the script runs and inserted into the XAML:

...
$Type = Add-Type -TypeDefinition $cSharpSource -ReferencedAssemblies $Assemblies -PassThru
$AssemblyName = $Type.Assembly.Fullname.Split(",",2)[0] 

Add-Type -AssemblyName PresentationFramework

[xml]$XAML = @"
<Window x:Name="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="650" Width="300" FontSize="11"
    xmlns:local="clr-namespace:myNamespace;assembly=$($AssemblyName)">
...

I'm not sure how hack-y this is, but it works and allows for all code to remain in plaintext and no software tools (besides a text editor) are required to continue development.

I may not have searched well enough, but I couldn't find a single example of this kind of thing online. Hope this helps somebody out there!

Sign up to request clarification or add additional context in comments.

2 Comments

You can probably do the exact same in pure PS. but you'd have to know the assembly name your TemplateSelector is running under. try to print this typeof(myDataTemplateSelector).Assembly.FullName and see what you get.
@Slimerecipe Thank you again! I used PS to accomplish this. Edited my own answer w/ the specifics. In brief, the assembly name is dynamically generated when the code is loaded in memory and so the XAML reference also must be dynamically generated when the script runs.
1

You need to specify the assembly and namespace. local="clr-namespace:myNamespace" just specify the namespace. I'm not sure how it works in PS environment thou.

https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/xaml-namespaces-and-namespace-mapping-for-wpf-xaml

2 Comments

Thanks for your advice. I have compiled the C# code in Visual Studio with no errors. The resultant 'myDataSelectorTemplateLibrary.dll' file was copied to the same directory as the script file and this referenced in XAML: xmlns:local="clr-namespace:myNamespace;assembly=myDataTemplateSelectorLibrary" Unfortunately, I get the same error upon running the script.
Turns out the error was due to me loading the assembly improperly previously in the same PowerShell console. There was no error message saying it was already loaded when attempting to do it again and I assumed it was loaded correctly. After loading a new PowerShell console window and re-loading the assembly, it worked. Thanks!
0

To reference classes in the current script this does the trick.

xmlns:local="clr-namespace:$([YourClass].Namespace);assembly=$([YourClass].Assembly.FullName)"

Then you can create a static resource and voila :)

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.