Roslyn+T4+EnvDTE專案完全自動化 (一)

whousee發表於2021-11-28

前言

以前做一個金融軟體專案,軟體要英文、繁體版本,開始甲方弄了好幾個月,手動一條一條替換,發現很容易出錯,因為有金融專業術語,字串在不同語義要特殊處理,第三方工具沒法使用。最後我用Roslyn寫了一個工具,只需10分鐘就能翻譯整個軟體,100%準確

做完上個專案發現Roslyn還可以深度開發,寫了一個工具:程式碼助手,可解決專案所有瑣碎重複性操作,程式碼完全自動化 

原理

Roslyn是啥?

XmlDocument,XDocument可以解析xml,同樣 Roslyn 可解析專案中C#程式碼。c#常用外掛ReSharper,只能重構一些很規範的程式碼(生成IEqualityComparer,IComparer介面...),用Roslyn可以自動化業務程式碼

自己寫 程式碼助手

 

 

 

一,類->檢視->增刪改查 全自動

需求:一個資料庫所有表的增刪改查

實現:Roslyn+T4實現類自動生成增刪改查介面

 

 

 生成最終效果

 

 

 每個屬性對應不同的控制元件:

 

自動生成單個物件編輯預覽

 

 

 

 

 

Roslyn+T4實現 

  1. EnvDTE獲取當前開啟專案
  2. Roslyn api CodeAnalysis 分析當前專案
  3. 獲取程式碼Symbol
  4. T4程式碼自動化 

T4主要程式碼

<#@ include file="Include\base.t4" #>
<#@ include file="Include\CodeAnalysis.t4" #>
<#
    param.Task = DoAsync();
#>
<#+
    Dictionary<string, ClassEntry> dic;
    int depthLevel = 2;                         //巢狀類展開深度
    
    async Task<string> DoAsync()
    {
        var Modules = "Modules";
        var ns = "Test.Database";               //獲取名稱空間 Test.Database 所有類
        var DbContextName = "sakilaEntities";   //測試資料庫
        var skipClass = new[]
        {
            DbContextName,
        }.ToHashSet();                          //排除類

        var modulesProjectItem = new ProjectItemEntry(@"Test\View\Modules");        //獲取專案view的路徑
        var viewModelProjectItem = new ProjectItemEntry(@"Test\ViewModel\Modules"); //獲取專案ViewModel的路徑
        /*
        modulesProjectItem.Delete();
        viewModelProjectItem.Delete();
        return "";
        */

        var analysisCore = await param.TryAnalysisSolutionAsync(cancellationToken); //Roslyn 分析當前工程
        var solution = analysisCore.Workspace.CurrentSolution;                      //Roslyn 的解決方案

        await TypeSymbols.InitializeTypeSymbolsAsync(solution, cancellationToken);

        //獲取名稱空間 Test.Database 所有類
        var list = await solution.GetAllSymbolsAsync((project, symbol) =>
        {
            var displayString = symbol.ContainingNamespace.ToDisplayString();
            if (displayString == ns && !skipClass.Contains(symbol.Name))
            {
                return true;
            }
            return false;
        });
        var items = list.Select(p =>
        {
            var entry = new ClassEntry(p);
            entry.DoProperty();
            return entry;
        }).ToList();
#>

 

T4生成文字

<#+
//xaml
{
        var notMapHashSet = new HashSet<string>();  //每個屬性型別對就的控制元件,如果沒有對映,寫日誌
        double index = 0;
        dic = items.ToDictionary(p=> p.Name);
        foreach (var c in items)
        {
            index++;

            var xamlOutput = StartNewFile(modulesProjectItem.GetRelativePath(c.MainViewPair.XamlFileName), true); //開啟一個新檔案
            param.Log($@"{index / items.Count:P1}:{c.MainViewPair.ClassName}");
//xaml
#>
<UserControl
    x:Class="<#=modulesProjectItem.Namespace#>.<#=c.MainViewPair.ClassName#>"
    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:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
    xmlns:custcombobox="clr-namespace:Test.View.CustComboBox"
    xmlns:local="clr-namespace:<#=modulesProjectItem.Namespace#>"
    xmlns:vm="clr-namespace:<#=viewModelProjectItem.Namespace#>"
    d:DataContext="{d:DesignInstance IsDesignTimeCreatable=True,
                                     Type={x:Type vm:<#=c.MainViewPair.ViewModelClassName#>}}"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d"
    >
..........................................
<#+
public class XamlCsPair
{
    public readonly string ClassName = string.Empty;            //類名
    public readonly string XamlFileName = string.Empty;         //View絕對路徑
    public readonly string CsFileName = string.Empty;           //ViewModel絕對路徑

    public readonly string ViewModelClassName = string.Empty;   //View類名
    public readonly string ViewModelFileName = string.Empty;    //ViewModel 檔名
    public XamlCsPair(string className, string vewModelClassName)
    {
        ClassName = className;
        className = className.GetValidFileName("_");            //類名到檔名,移除非法字元

        XamlFileName = $"{className}.xaml";
        CsFileName = $"{XamlFileName}.cs";
        ViewModelClassName = vewModelClassName;
        ViewModelFileName = $"{ViewModelClassName}.cs";
    }
}
public class ClassEntry
{
    public ProjectSymbolEntry Entry { get; }
    public INamedTypeSymbol Symbol { get; }             //類Symbol
    public readonly string Name = string.Empty;         //類的名稱
    public readonly string NameZh = string.Empty;       //類的中文名稱
    public readonly ITypeSymbol Type;                   //類的型別Symbol
    public readonly string TypeName = string.Empty;     //類的型別名稱

    public readonly string ClassName = string.Empty;    //目標類名
    
    public readonly XamlCsPair MainViewPair;
    public readonly XamlCsPair EditViewPair;
    
    public List<PropertyEntry> Properties { get; set; } = new List<PropertyEntry>();//類的屬性集合
    public ClassEntry(ProjectSymbolEntry entry)
    {
        Entry = entry;
        var symbol = entry.Symbol;
        Symbol = symbol;
        Name = symbol.Name;
        NameZh = Name.ToZh(cacheMode: CacheMode.OneWay); //百度api英文翻譯中文
        Type = symbol.GetTypeSymbol();
        TypeName = Type.GetDisplayShortName();

        ClassName = Name.ToValidIdentifier(removeChars:"_".ToCharArray());
        MainViewPair = new XamlCsPair($"{ClassName}View", $"{ClassName}ViewModel");
        EditViewPair = new XamlCsPair($"{ClassName}ViewEditorWindow", $"{ClassName}ViewEditorViewModel");
    }

    public void DoProperty()
    {
        Properties.Clear();
        foreach (var member in Symbol.GetMembers().OfType<IPropertySymbol>())
        {
            Properties.Add(new PropertyEntry(this, member));
        }
    }
}
public class PropertyEntry
{
    public ClassEntry Class { get; }
    public IPropertySymbol Symbol { get; }          //屬性的Symbol
    public readonly string Name = string.Empty;
    public readonly string NameZh = string.Empty;   //屬性中文名稱
    public readonly ITypeSymbol Type;               //屬性型別
    public readonly string TypeName = string.Empty; //屬性型別名稱

    public PropertyEntry(ClassEntry entry, IPropertySymbol symbol)
    {
        Class = entry;
        Symbol = symbol;
        Name = symbol.Name;
        NameZh = Name.ToZh(cacheMode: CacheMode.OneWay);//百度api英文翻譯中文
        Type = symbol.GetTypeSymbol();                  //屬性Roslyn型別
        TypeName = Type.GetDisplayShortName();          //屬性Roslyn名稱
    }
}
public class ProjectItemEntry
{
    public ProjectItem ProjectItem { get; } //dte對應的一個專案檔案
    public string Namespace { get; }        //ProjectItem的名稱空間
    public string FileName { get; }         //專案檔案絕對路徑

    public ProjectItemEntry(string projectRelativePath)
    {
        ProjectItem = dte.GetProjectItemByRelativePath(projectRelativePath).TryCreateDir();
        Namespace = (string)ProjectItem.Properties.Item("DefaultNamespace").Value;
        FileName = ProjectItem.GetFileName();

    }
    public void Delete()
    {
        ProjectItem.DeleteChildren();
        FileName.DeleteSubFiles();
    }
    public string GetRelativePath(string relativePath)
    {
        return Path.Combine(FileName, relativePath);
    }
}
#>

  

 

相關文章