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.
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!
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
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>
Related
I'm designing a small encoding/decoding application that uses System.Configuration.Cryptography.ProtectedData methods at its core.
To let the user select the data protection scope, I've included radio buttons such as this one:
<RadioButton x:Name="rbtMachineScope"
GroupName="rbgProtectionScope"
IsChecked="{Binding Path=ProtectionScope, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static crypto:DataProtectionScope.LocalMachine}}"
Content="_Machine"/>
Obviously, the XAML file begins like this:
<Window x:Class="DecodeEncodeApp.MainWindow"
xmlns:crypto="clr-namespace:System.Security.Cryptography;assembly=System.Core"
DataContext="{Binding Path=EncryptionModule}">
<Window.Resources>
<!-- Converters -->
<conv:EnumToBooleanConversion x:Key="EnumToBoolean" />
</Window.Resources>
(edited for brevity)
EncryptionModule is an object of the type TextEncryption:
public class TextEncryption : INotifyPropertyChanged
{
public DataProtectionScope ProtectionScope;
}
(In the interest of saving more space, trust me that ProtectionScope comes with a private field that is duly used in its get and set methods in such a way that bindings are possible. I've checked that.)
(Also, that EnumToBooleanConversion implements IValueConverter quite banally.)
Compiling gets the following message:
"Unknown build error, MC3050: type 'DataProtectionScope' not found." (Translated back from the local French, sorry if inaccurate.)
Except that in TextEncryption (that has using System.Security.Cryptography;), DataProtectionScope is found.
I use VS2019 and my project uses .Net 5.0.
What did I do wrong?
I have a peculiar problem regarding internal classes used from xaml.
The problem is as follows:
We decided to strong-name all our assemblies. After the strong-naming, all but one project compiles without a hitch under VS 2017. The situation is as follows:
Assembly Company.Component - Compiles without problems, and exposes its internals as follows:
[assembly: InternalsVisibleTo("Company.Component.Test, PublicKey={omitted}")]
[assembly: InternalsVisibleTo("Company.OtherLibrary, PublicKey={omitted}")]
Assembly Company.Component.Test - Compiles without problems, and can access internals in Company.Component just fine. All tests pass.
Company.OtherLibrary - Does not compile at all, due to the following error in one of its xaml files:
Only public or internal classes can be used within markup. 'Converter1' is not public or internal.
The xaml file in question can be seen below (with names changed):
<ns:class x:Class="Company.OtherLibrary.Class"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:converters="clr-namespace:Company.Component.Converters;assembly=Company.Component"
xmlns:viewModels="clr-namespace:Company.OtherLibrary.ViewModels"
xmlns:ns="clr-namespace:Company.Component;assembly=Company.Component"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
d:DataContext="{d:DesignInstance d:Type=viewModels:ViewModelClass}"
Style="{StaticResource {x:Type ns:Style}}">
<UserControl.Template>
<ControlTemplate>
<DockPanel>
<DockPanel.Resources>
<converters:Converter1 x:Key="Converter1" />
<converters:Converter2 x:Key="Converter2" />
<converters:Converter3 x:Key="Converter3" />
<converters:Converter4 x:Key="Converter4" />
</DockPanel.Resources>
...
</DockPanel>
</ControlTemplate>
</UserControl.Template>
Now to answer some questions:
The public key is the correct public key (not only the token) of the OtherLibrary assembly. It is in fact the same one used for the test assembly.
The assembly name for OtherLibrary is the same as the one in its properties.
The code worked before the strong naming. No changes have been made to any code file in the projects (bar AssemblyInfo.cs). I have checked with VS 2017 git compare. The edited lines are the ones shown, and the edit is to insert public keys.
The xaml-namespaces can sometimes be found, and sometimes not. This only goes for the namespaces that have no public classes.
All converters seen in the dock panel resources are internals in the Company.Component assembly. They all give the following error:
The type '{type}' is not accessible.
All libraries are built against the same version of the framework/dlls.
The assembly reference to Company.Component have 'copy local' set to true.
What I've tried
Clearing xaml shadow-cache
Cleaning and rebuilding.
Changing active build-configuration (both platform and Debug/Release).
Manually cleaning all bin and obj directories for all projects.
Restarted both computer and VS 2017.
All of the above at once.
There is something very simple that I've obviously overlooked. Can someone please help me figure out what is going on here? I am not allowed (for reasons left out) to change the component library other than strongly naming it.
I have really strange problem. I've created WPF project in 2012 or 2013 VS it doesn't matter. I use .NET 4.5.
I add a new Activity (Workflow class) to that project. Its name is CustomActivity.
Then I add a new class that has got an attached property, example below:
public class AttachedObject : DependencyObject
{
public static readonly DependencyProperty NameProperty = DependencyProperty.RegisterAttached(
"Name",
typeof(string),
typeof(AttachedObject),
new FrameworkPropertyMetadata(
string.Empty,frameworkPropertyMetadataOptions.AffectsRender));
public static void SetName(ContentControl element, string value)
{
element.SetValue(NameProperty, value);
}
public static string GetName(ContentControl element)
{
return (string)element.GetValue(NameProperty);
}
}
The last step is to change MainWindow class that way:
public MainWindow()
{
InitializeComponent();
var activity = new CustomActivity();
}
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfApplication1="clr-namespace:WpfApplication1;assembly=WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ContentControl wpfApplication1:AttachedObject.Name="MainArea"/>
</Grid>
</Window>
The problem is it doesn't compile because of below error:
Error 1 The type or namespace name 'CustomActivity' could not be found (are you missing a using directive or an assembly reference?) WpfApplication1\MainWindow.xaml.cs 13 32 WpfApplication1
CustomActivity has a default namespace. In obj folder there is CustomActivity.g.cs generated, so I have no idea what's going on.
It's 100% reproducible. When I remove using of CustomActivity or using of AttachedObject from xaml then the problem disappear.
Try replacing this:
xmlns:wpfApplication1="clr-namespace:WpfApplication1;assembly=WpfApplication1"
with this
xmlns:wpfApplication1="clr-namespace:WpfApplication1"
The error you're seeing is due to a "known" issue in WPF applications that xaml namespaces that reference clr namespace from current assembly your in don't require the full assembly qualified name. If you were to declare a xaml namespace that references a clr namespace from another assembly, than you would have to specify the full name (with the ;[assemblyname] syntax).
Workflow Foundation has nothing to do with it.
EDIT:
Didn't realize it was a xaml activity.
But still, you can make it work, maybe, with a few hacks, but I wouldn't recommend it.
The reason you get that error is due to the different code generation and build action VS uses when creating xaml artifacts for WPF (Page):
System.Windows.Application.LoadComponent(this, resourceLocater);
and when creating xaml activities (XamlAppDef):
typeof(CustomActivity).Assembly.GetManifestResourceStream(resourceName);
If you turn your CustomActivity xaml build action to Page, the whole thing will compile - but i'm guessing something else might be broken someplace else...or not, who knows. My guess is that these two kinds of xaml were not meant to live together in a VS WPF application project template. But you can still define activities in a WF activity library, that way your activities will also be more easily reusable for other projects, WPF, console or even services.
I have the same issue under Visual Studio 2017.
The problem in my case is that Visual Studio is not compiling the Workflow activities before the code that use them.
To fix it, what I did is to move all workflows to other project dll, so visual Studio is forced to compile the workflows before the classes that make use of them.
I'm hoping someone can help me with an odd issue. Developing a WPF .NET Framework 4.0 application, receiving the message:
No matching constructor found on type. You can use the Arguments or FactoryMethod directives to construct the type when attempting to use the following code.
UserControl:
<Grid>
<GroupBox Header="Diagram">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<designer:DesignerCanvas Focusable="true" x:Name="MyDesigner"
Background="{StaticResource WindowBackgroundBrush}" Margin="10"
FocusVisualStyle="{x:Null}"
ContextMenu="{StaticResource DesignerCanvasContextMenu}" />
</ScrollViewer>
</GroupBox>
</Grid>
Class:
/// <summary>
/// The designer canvas.
/// </summary>
public partial class DesignerCanvas : Canvas
{
public DesignerCanvas()
{
....
}
}
As far as I can see there doesn't appear to be anything wrong with the code.
Thanks,
John
Update:
Turns out this is masking the underlying exception. I stripped out anything in the constructor in DesignerCanvas and it worked. I then re-added setting up command bindings in a separate method and received:
System.MissingMethodException: Method not found: Void System.Windows.Input.CommandBinding..ctor(System.Windows.Input.ICommand, System.Windows.Input.ExecutedRoutedEventHandler)
The constructor included attempts to setup some command bindings. e.g.
public void SetupCommandDefaults()
{
this.CommandBindings.Add(new CommandBinding(ApplicationCommands.New, this.New_Executed));
}
I'm uninstalling and reinstalling the .NET Framework 4.0 in the faint hope that this will fix the problem.
Turns out after much pain re-installing .NET Framework 4, applying various KB patches up to and including the runtime 4.0.3. the problem was caused by an ILMerge msbuild step.
The build process included a msbuild task after build to merge the main executable with some dll's.
I removed this ILMerge step and the error disappeared.
Not sure why this occured on this environment and not on any of our test machines but its working now.
I've created the following XAML, approximately (shortened for brevity):
<Window ...
xmlns:Models="clr-namespace:Project.Presentation.Models;assembly=Project"
...>
<Window.Resources>
<Models:ProfileCollection x:Key="Profiles" />
</Window.Resources>
</Window>
ProfileCollection is defined as, simply:
public class ProfileCollection : ObservableCollection<Profile>
{
public ProfileCollection()
{
foreach (Profile p in Configuration.Instance.Profiles)
this.Add(p);
}
// code that handles static added/removed events
}
This follows the requirements set forth in XAML and Custom Classes on MSDN.
However, when I try to compile I get this error:
error MC3074: The tag 'ProfileCollection' does not exist in XML namespace 'clr-Project.Presentation.Models;assembly=Project'. Line 18 Position 7.
I've also tried:
<Window ...
xmlns:SystemCollections="clr-namespace:System.Collections;assembly=mscorlib"
...>
<Window.Resources>
<SystemCollections:ArrayList x:Key="arrayList" />
</Window.Resources>
</Window>
That works fine.
public class SomeList : ArrayList { public SomeList() { } }
I get the same error trying to use this object. It's the same error as before.
<Models:SomeList x:Key="arrayList" /> <!-- MC3074 -->
Is 'ProfileCollection' class placed in the namespace 'Project.Presentation.Models? Also if XAML and class are both in 'Project' assembly, try to remove "assembly=Project" from xmlns declaration
Good luck ;)
I got the same error. The reason were different .NET Frameworks (assembly had 4.6.1 and my base project had 4.5.1).
Maybe this information helps others (the error code is not very precise in my mind...).