기본 콘텐츠로 건너뛰기

[VSIX] Visual Studio 확장에 대해서... #2 템플릿 추가

이전 게시글에서 생성한 패키지를 활용하기 위한 첫 번째 기능으로 INI 파일을 추가할 수 있는 Item Template Project를 추가해 보도록 한다.

Item Template 활용

Create Item Template Project

패키지 솔루션을 선택하고 "새 프로젝트 추가" 를 통해서 아래의 그림과 같이 C# Item Template Project를 추가하도록 한다.


프로젝트 이름은 "INIParserTemplate" 이라고 지정하고 "확인" 버튼을 누르면 패키지 솔루션에 "C# Item Template" 처리용 프로젝트가 생긴 것을 확인할 수 있다.



Add Template file and class

생성된 INIParserTemplate 프로젝트를 선택하고 "새 항목 추가" 를 통해서 "iniTemplate.tini" 라는 이름으로 텍스트 파일을 생성한다.

그리고 추가된 템플릿용 파일의 내용은 아래와 같이 설정하도록 한다.

[INISetup]
Locale=ko-KR

이제 템플릿 파일을 처리하기 위해서 기본으로 생성되어 있던 "Class1.cs" 파일을 "iniTemplate.cs" 라는 이름으로 변경하고 클래스 파일의 속성을 아래의 그림과 같은지 확인하도록 한다.


이제 추가한 파일들이 동작할 수 있으려면 *.vstemplate 파일을 통해서 항목 추가 작업에서 사용할 템플릿을 연결해 주어야 한다. 아래의 그림과 같이 해당 파일을 수정해 주도록 한다.


위의 설정 내용의 기능은 다음과 같이 이해하면 된다.
  • ReplaceParameters - 템플릿 파일 내에서 사용할 수 있는 템플릿 처리 파라미터를 사용한다는 의미로 Item Template이 처리될 때 Visual Studio로 부터 템플릿 파일에 보내지는 키/값 쌍의 사전 개체를 통해서 값을 설정한다는 의미다.
  • TargetFileName - 원본 (이 경우는 iniTemplate.tini 파일)을 읽어서 템플릿 처리를 수행한 결과를 저장할 대상 파일 명을 나타낸다. $fileinputname$ 은 "새 항목 추가" 다이얼로그에서 지정한 이름을 ReplaceParameters로 전달된 값으로 치환하다는 의미다. 위의 두 번째에 보면 "\"를 이용해서 경로와 같이 지정한 부분을 확인할 수 있는데, 이렇게 처리하면 *.tini 파일의 서브 파일로 생성되는 파일을 의미한다.
  • ItemType - 옵션으로 지정할 수 있는 설정으로 "Content"로 설정하면 컴파일도 포함리소스도 아닌 일반 파일로 추가되는 것을 의미한다. 생략하면 해당 파일의 확장자를 기준으로 자동으로 설정이 처리된다.
  • InnerText - <ProjectItem>...</ProjectItem> 사이에 설정되는 파일은 원본 템플릿 파일로 생성할 파일의 원본 구실을 하게 된다.
그리고 마지막으로 *.vstemplate 파일의 <DefaultName>...</DefaultName> 사이의 내용을 만들어질 파일을 식별할 수 있는 이름으로 변경한다. 여기서는 Class.cs 를 "INITemplate.tini" 라고 사용하도록 한다. 확장자가 tini 인 것은 보통 템플릿이라는 것을 식별하기 위한 용도로 큰 의미는 없다.

이제 Item Template 으로 사용할 구조는 만들었으므로 실제 코드로 사용할 INITemplate.cs 파일의 내부를 구성하여야 한다. 기존에 있던 내용을 모두 지우고 아래의 코드로 교체하도록 한다. 이 코드의 내용은 생성된 ini 파일을 처리하는 (Load/Save 등) 코드가 생성될 수 있도록 하기 위한 것이다.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;

$if$ ($targetframeworkversion$ >= 3.5)using System.Linq;
$endif$

namespace $rootnamespace$ {
    public class $safeitemrootname$ {
        #region Fields
  
        private readonly Dictionary<string, Dictionary<string, string>> content;

        #endregion

        #region Constructors
  
        public $safeitemrootname$(string path, CultureInfo culture) {
            this.IniPath = path;
            this.Culture = culture;
            this.content = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
            if (!File.Exists(path)) this.Save();
            this.Load();
        }

        #endregion

        #region Properties

        public string IniPath { get; private set; }
        public CultureInfo Culture { get; private set; }
   
        #endregion

        #region Methods
  
        public void Load() {
            string section = null;
            this.content.Clear();
            foreach(var line in File.ReadAllLines(this.IniPath)) {
                var prepared = line.Trim();
                if (line.StartsWith("[") && line.EndsWith("]")) {
                    section = prepared.Substring(1, prepared.Length - 2);
                    this.content.Add(section, new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase));
                } else if (!line.StartsWith("#") && !string.IsNullOrWhiteSpace(line) && section != null) {
                    var expl = line.IndexOf('=');
                    if (expl > 0)
                    this.content[section][line.Substring(0, expl)] = line.Substring(expl + 1);
                }
            }
        }

        public void Save() {
            var text = string.Empty;
            text += this.WriteLine("[INISetup]");
            text += this.WriteLine("Culture=" + this.Culture.Name);

            foreach(var category in this.content) {
                var cat = category;
                if (cat.Key != "INISetup") {
                    text += this.WriteLine("[" + cat.Key + "]");
                    foreach(var val in cat.Value) {
                        text += this.WriteLine(val.Key + "=" + val.Value);
                    }
                }
            }
            File.WriteAllText(this.IniPath, text);
        }
 
        public string GetValue(string id, string section) {
            if (!this.content.ContainsKey(section)) return null;
            if (!this.content[section].ContainsKey(id)) return null;
            return this.content[section][id];
        }

        public string WriteLine(string line) {
            return line + "\r\n";
        }

        #endregion
    }
}

Include the Template Project to the Package Project

패키지가 배포(설치)될 때 위에서 구성한 템플릿 프로젝트가 포함될 수 있도록 아래의 그림과 같이 패키지 프로젝트의 "참조" 에 "INIParserTemplate" 프로젝트를 추가하도록 한다.


그리고 참조된 프로젝트의 어셈블리의 속성을 다음과 같이 설정하도록 한다.


위의 참조 설정은 다음과 같은 의미를 가지는 것이라고 생각하면 된다.

  • Output Groups Include in VSIX - "TemplateProjectOutputGroup;" 이라고 설정하는 것은 지정된 그룹 이름으로 항목이 위치한다는 의미를 가지며 패키지 프로젝트의 배포 설정에 사용된다.(*.vsixmanifest 파일 연계)
  • Reference Output Assembly - "false" 로 설정하는 것은 템플릿이 Zip 파일로 생성이 될 때 참조되는 어셈블리들이 같이 포함되어야 하는지 여부를 의미하는 것이다.
  • Template Type - "Item" 은 Item Template 으로 생성되어야 한다는 것을 의미하는 것이다.
  • VSIX Sub Path - "ItemTemplates\FDTWorks" 설정은 Visual Studio 의 마법사에 표시될 경로를 의미하는 것이다.
이제 패키지 프로젝트의 배포 관련 설정을 아래와 같이 수정하도록 한다.



F5 또는 Ctrl + F5 를 눌러서 실행을 해 보면 새로운 Visual Studio 가 실행이 되고, 열려진 테스트 프로젝트에서 "새 항목 추가"를 선택하면 아래의 그림과 같이 패키지를 통해서 배포된 Item Template 가 보여지는 것을 확인할 수 있다.


위의 그림에서 왼쪽에 보여지는 것은 위에서 INIParserTemplate 참조 어셈블리의 설정에서 VSIX Sub Path 로 지정한 정보를 기준으로 표시된 것이고, 그 아래에 "CSharp" 으로 보여지는 것은 언어 선택에 따라서 자동으로 분류된 것이다.

"추가 버튼" 을 눌러서 추가를 하면 대상 프로젝트에 아래의 그림과 같이 INI 파일과 이 파일을 처리하기 위한 클래스가 연계되서 생성된 것을 확인할 수 있다.


위의 그림에서 보는 것과 같이 INI 파일 밑으로 클래스가 생기는 설정은 위에서 설정했던 *.vstemplate 파일의 ProjectItem 요소에서 "TargetFileName" 설정에 의한 것이다.

*** 물론 위의 처리 부분은 Visual Studio에서 제공하는 T4 를 이용하면 더 쉽게 처리가 가능하지만, 지금은 패키지를 통해서 원하는 처리를 하는 것이 주 목적이므로 설정을 어떻게 하면 되는지를 검증하기 위한 것으로 진행을 하였다.

댓글