Xamarin,如何为项目中的图像文件名生成常量?

Jul*_*les 8 xamarin xamarin.forms visual-studio-mac

我正在寻找一种方法来为我的项目中的图像文件名生成常量类 c# 文件。因此,我可以在代码和 xaml、运行时和设计时使用它们,当重新生成类时(当图像文件已更改时),这将突出潜在问题。

在过去的项目中,我们使用 TypeWriter,它使用反射来查看项目文件并运行我们自己的脚本以根据我们脚本中定义的模板生成代码文件。

我讨厌魔法弦,只想要这种额外的安全性。

我想是完整的,以及 Xamarin 共享项目,它也需要在 iOS 和 Android 项目中可用。

理想情况下,我想在文件更改时触发脚本,但这可以手动运行。

我使用的是 Visual Studio for Mac,因此 Nuget 包/扩展较少。

我希望我可以轻松扩展此功能以在 app.xml.cs 中为颜色创建常量。

Roa*_*ald 1

这是一个关于如何使用 msbuild 而不是像我的其他答案中那样的源生成器来完成此操作的示例。

自定义任务:

public class GeneratorTask : Task
{
    [Required]
    public string OutputFile { get; set; } = "";

    [Required]
    public ITaskItem[] SourceFiles { get; set; } = Array.Empty<ITaskItem>();

    [Required]
    public string TypeName { get; set; } = "";

    public override bool Execute()
    {
        if (string.IsNullOrWhiteSpace(OutputFile))
        {
            Log.LogError($"{nameof(OutputFile)} is not set");
            return false;
        }

        if (string.IsNullOrWhiteSpace(TypeName))
        {
            Log.LogError($"{nameof(TypeName)} is not set");
            return false;
        }

        try
        {
            var files = SourceFiles
                .Select(item => item.ItemSpec)
                .Distinct()
                .ToArray();

            var code = GenerateCode(files);

            var target = new FileInfo(OutputFile);

            if (target.Exists)
            {
                // Only try writing if the contents are different. Don't cause a rebuild
                var contents = File.ReadAllText(target.FullName, Encoding.UTF8);
                if (string.Equals(contents, code, StringComparison.Ordinal))
                {
                    return true;
                }
            }

            using var file = File.Open(target.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
            using var sw = new StreamWriter(file, Encoding.UTF8);

            sw.Write(code);
        }
        catch (Exception e)
        {
            Log.LogErrorFromException(e);
            return false;
        }

        return true;
    }

    // Super simple codegen, see my other answer for something more sophisticated.
    string GenerateCode(IEnumerable<string> files)
    {
        var (namespaceName, typeName) = SplitLast(TypeName, '.');

        var code = $@"
// Generated code, do not edit.
namespace {namespaceName ?? "FileExplorer"}
{{
    public static class {typeName}
    {{
        {string.Join($"{Environment.NewLine}\t\t", files.Select(GenerateProperty))}
    }}
}}";

        static string GenerateProperty(string file)
        {
            var name = file
                .ToCharArray()
                .Select(c => char.IsLetterOrDigit(c) || c == '_' ? c : '_')
                .ToArray();

            return $"public static readonly string {new string(name)} = \"{file.Replace("\\", "\\\\")}\";";
        }

        static (string?, string) SplitLast(string text, char delimiter)
        {
            var index = text.LastIndexOf(delimiter);

            return index == -1
                ? (null, text)
                : (text.Substring(0, index), text.Substring(index + 1));
        }

        return code;
    }
}
Run Code Online (Sandbox Code Playgroud)

FileExplorer.targets 文件:

public class GeneratorTask : Task
{
    [Required]
    public string OutputFile { get; set; } = "";

    [Required]
    public ITaskItem[] SourceFiles { get; set; } = Array.Empty<ITaskItem>();

    [Required]
    public string TypeName { get; set; } = "";

    public override bool Execute()
    {
        if (string.IsNullOrWhiteSpace(OutputFile))
        {
            Log.LogError($"{nameof(OutputFile)} is not set");
            return false;
        }

        if (string.IsNullOrWhiteSpace(TypeName))
        {
            Log.LogError($"{nameof(TypeName)} is not set");
            return false;
        }

        try
        {
            var files = SourceFiles
                .Select(item => item.ItemSpec)
                .Distinct()
                .ToArray();

            var code = GenerateCode(files);

            var target = new FileInfo(OutputFile);

            if (target.Exists)
            {
                // Only try writing if the contents are different. Don't cause a rebuild
                var contents = File.ReadAllText(target.FullName, Encoding.UTF8);
                if (string.Equals(contents, code, StringComparison.Ordinal))
                {
                    return true;
                }
            }

            using var file = File.Open(target.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
            using var sw = new StreamWriter(file, Encoding.UTF8);

            sw.Write(code);
        }
        catch (Exception e)
        {
            Log.LogErrorFromException(e);
            return false;
        }

        return true;
    }

    // Super simple codegen, see my other answer for something more sophisticated.
    string GenerateCode(IEnumerable<string> files)
    {
        var (namespaceName, typeName) = SplitLast(TypeName, '.');

        var code = $@"
// Generated code, do not edit.
namespace {namespaceName ?? "FileExplorer"}
{{
    public static class {typeName}
    {{
        {string.Join($"{Environment.NewLine}\t\t", files.Select(GenerateProperty))}
    }}
}}";

        static string GenerateProperty(string file)
        {
            var name = file
                .ToCharArray()
                .Select(c => char.IsLetterOrDigit(c) || c == '_' ? c : '_')
                .ToArray();

            return $"public static readonly string {new string(name)} = \"{file.Replace("\\", "\\\\")}\";";
        }

        static (string?, string) SplitLast(string text, char delimiter)
        {
            var index = text.LastIndexOf(delimiter);

            return index == -1
                ? (null, text)
                : (text.Substring(0, index), text.Substring(index + 1));
        }

        return code;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后在你的 .csproj 中:

<PropertyGroup>
    <FileExplorerOutputFile>$(MSBuildThisFileDirectory)Assets.g.cs</FileExplorerOutputFile>
    <FileExplorerTypeName>FileExplorer.Definitions.Assets</FileExplorerTypeName>
</PropertyGroup>

<ItemGroup>
    <FileExplorerSourceFiles Include="assets\**\*" />
</ItemGroup>

<ItemGroup>
    <ProjectReference Include="..\FileExplorer\FileExplorer.csproj" />
</ItemGroup>

<Import Project="..\FileExplorer\FileExplorer.targets" />
Run Code Online (Sandbox Code Playgroud)

这是带有完整示例的 github 存储库:msbuild-fileexplorer


在 VS 2019 和 Rider 中进行了测试。
请记住,我不是 msbuild 专家,这个解决方案可能还可以改进。