按照这个问题,我想根据预处理器指令在构建时有条件地包含/排除 XAML 元素,类似于 #if/#else
在 C# 中的工作方式。我尝试过使用 XSLT 来转换 XAML,但它没有按预期工作。我想要实现的目标
.csproj
中定义预处理器常量:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DefineConstants>DEBUG;TRACE;fix_issue_001</DefineConstants>
</PropertyGroup>
然后在 XAML 中,我想有条件地包含基于这些常量的元素:
<Window x:Class="XamlPreprocessDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<!-- fix_issue_001:true -->
<Button Content="Original version" Click="Button_Click" />
<!-- end fix_issue_001:true -->
<!-- fix_issue_001:false -->
<Button Content="New version" Background="LightGreen" Click="Button_Click" />
<!-- end fix_issue_001:false -->
</StackPanel>
</Window>
我尝试的解决方案
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:param name="DefineConstants" select="''"/>
<!-- Identity transform -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<!-- Skip end markers -->
<xsl:template match="comment()[starts-with(normalize-space(.), 'end fix_')]"/>
<!-- Handle conditional comments -->
<xsl:template match="comment()[starts-with(normalize-space(.), 'fix_')]">
<xsl:variable name="content" select="normalize-space(.)"/>
<xsl:variable name="endMarker" select="concat('end ', substring-before($content, ':'))"/>
<xsl:choose>
<!-- True condition -->
<xsl:when test="contains($content, ':true')">
<xsl:variable name="constant" select="substring-before($content, ':true')"/>
<xsl:if test="contains($DefineConstants, $constant)">
<xsl:variable name="endComment" select="following-sibling::comment()
[normalize-space(.) = concat('end ', $constant, ':true')][1]"/>
<xsl:apply-templates select="following-sibling::node()
[generate-id(preceding-sibling::comment()
[contains(., concat($constant, ':true'))][1]) = generate-id(current())
and following-sibling::comment()
[normalize-space(.) = concat('end ', $constant, ':true')][1]
and generate-id(.) != generate-id($endComment)]"/>
</xsl:if>
</xsl:when>
<!-- False condition -->
<xsl:when test="contains($content, ':false')">
<xsl:variable name="constant" select="substring-before($content, ':false')"/>
<xsl:if test="not(contains($DefineConstants, $constant))">
<xsl:variable name="endComment" select="following-sibling::comment()
[normalize-space(.) = concat('end ', $constant, ':false')][1]"/>
<xsl:apply-templates select="following-sibling::node()
[generate-id(preceding-sibling::comment()
[contains(., concat($constant, ':false'))][1]) = generate-id(current())
and following-sibling::comment()
[normalize-space(.) = concat('end ', $constant, ':false')][1]
and generate-id(.) != generate-id($endComment)]"/>
</xsl:if>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
这是我的 MSBuild 集成 (XamlPreprocess.targets):
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskName="XamlPreprocessTask"
TaskFactory="RoslynCodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<InputFile ParameterType="System.String" Required="true" />
<OutputFile ParameterType="System.String" Required="true" />
<DefineConstants ParameterType="System.String" Required="true" />
<TransformFile ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System"/>
<Using Namespace="System.IO"/>
<Using Namespace="System.Xml"/>
<Using Namespace="System.Xml.Xsl"/>
<Code Type="Fragment" Language="cs">
<![CDATA[
try {
var transform = new XslCompiledTransform();
transform.Load(TransformFile);
var args = new XsltArgumentList();
args.AddParam("DefineConstants", "", DefineConstants);
var outputDir = Path.GetDirectoryName(OutputFile);
if (!Directory.Exists(outputDir))
Directory.CreateDirectory(outputDir);
using (var writer = new XmlTextWriter(OutputFile, System.Text.Encoding.UTF8)) {
writer.Formatting = Formatting.Indented;
transform.Transform(InputFile, args, writer);
}
return true;
}
catch (Exception ex) {
Log.LogError($"XAML preprocessing failed: {ex.Message}");
return false;
}
]]>
</Code>
</Task>
</UsingTask>
<PropertyGroup>
<XamlPreprocessDir>$(IntermediateOutputPath)PreprocessedXaml\</XamlPreprocessDir>
<XamlTransformFile>$(MSBuildThisFileDirectory)Transform.xslt</XamlTransformFile>
</PropertyGroup>
<Target Name="PreprocessXaml" BeforeTargets="MarkupCompilePass1;XamlMarkupCompilePass1">
<ItemGroup>
<XamlFiles Include="**\*.xaml"
Exclude="$(XamlPreprocessDir)**\*.xaml;**\.vshistory\**\*.xaml;App.xaml;bin\**\*.xaml;obj\**\*.xaml" />
</ItemGroup>
<MakeDir Directories="$(XamlPreprocessDir)" />
<XamlPreprocessTask
InputFile="%(XamlFiles.FullPath)"
OutputFile="$(XamlPreprocessDir)%(XamlFiles.RecursiveDir)%(XamlFiles.Filename)%(XamlFiles.Extension)"
DefineConstants="$(DefineConstants)"
TransformFile="$(XamlTransformFile)">
</XamlPreprocessTask>
<!-- Copy the preprocessed files to a temporary backup -->
<Copy SourceFiles="@(XamlFiles)"
DestinationFiles="@(XamlFiles->'$(XamlPreprocessDir)%(RecursiveDir)%(Filename).original%(Extension)')" />
<!-- Copy the preprocessed files over the originals -->
<Copy SourceFiles="@(XamlFiles->'$(XamlPreprocessDir)%(RecursiveDir)%(Filename)%(Extension)')"
DestinationFiles="@(XamlFiles)" />
</Target>
<Target Name="RestoreXaml" AfterTargets="MarkupCompilePass1;XamlMarkupCompilePass1">
<!-- Restore the original files -->
<Copy SourceFiles="@(XamlFiles->'$(XamlPreprocessDir)%(RecursiveDir)%(Filename).original%(Extension)')"
DestinationFiles="@(XamlFiles)" />
</Target>
<Target Name="CleanPreprocessedXaml" BeforeTargets="Clean">
<RemoveDir Directories="$(XamlPreprocessDir)" />
</Target>
</Project>
问题fix_issue_001
,两个按钮都会显示在最终应用程序中。我希望在定义
fix_issue_001
时仅看到“原始版本”按钮,未定义时仅看到“新版本”按钮。要求
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all">
<xsl:accumulator name="collect-to-delete" as="xs:boolean" initial-value="false()">
<xsl:accumulator-rule match="comment()[matches(., '^\s*fix_issue_[0-9]*:false\s*$')]" select="true()"/>
<xsl:accumulator-rule match="comment()[matches(., '^\s*end fix_issue_[0-9]*:false\s*$')]" select="false()"/>
</xsl:accumulator>
<xsl:accumulator name="delete" as="element()*" initial-value="()">
<xsl:accumulator-rule match="*[accumulator-before('collect-to-delete')]" select="$value, ."/>
</xsl:accumulator>
<xsl:mode on-no-match="shallow-copy" use-accumulators="collect-to-delete delete"/>
<xsl:template match="comment()[matches(., '^\s*fix_issue_[0-9]*:(true|false)\s*$')]"/>
<xsl:template match="comment()[matches(., '^\s*end fix_issue_[0-9]*:(true|false)\s*$')]"/>
<xsl:template match="*[accumulator-before('delete') intersect .]"/>
</xsl:stylesheet>
转变
<Window x:Class="XamlPreprocessDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<!-- fix_issue_001:true -->
<Button Content="Original version" Click="Button_Click" />
<!-- end fix_issue_001:true -->
<!-- fix_issue_001:false -->
<Button Content="New version" Background="LightGreen" Click="Button_Click" />
<!-- end fix_issue_001:false -->
</StackPanel>
</Window>
进入
<?xml version="1.0" encoding="UTF-8"?><Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="XamlPreprocessDemo.MainWindow" Title="MainWindow" Height="450" Width="800">
<StackPanel>
<Button Content="Original version" Click="Button_Click"/>
</StackPanel>
</Window>
例如,可以使用 .NET Framework/C# 和 Saxon HE 10 运行。而且它应该比 XSLT 1.0 更容易适应和调试。