Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법 (2) - 동적 메뉴 구성

지난 글에서 메뉴 추가 방법에 대한 기본적인 내용을 살펴봤습니다.

Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법
; https://www.sysnet.pe.kr/2/0/11184

그런데, 때로는 메뉴를 동적으로 구성해야 할 때가 있습니다. Visual Studio의 경우 대표적인 사례로 "File" / "Recent Files" 하위 메뉴를 들 수 있는데, 이를 위한 방법 역시 문서화가 너무 잘되어 있습니다. ^^

Dynamically Adding Menu Items
; https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2015/extensibility/dynamically-adding-menu-items

위의 글에 따라, 동적으로 추가되는 "매크로 명령어 목록"을 구성해 보겠습니다.

우선, 동적 메뉴로 펼쳐질 서브 메뉴의 부모가 되어줄 Menu가 있어야 하는데, 그 메뉴는 또한 Command 그룹에 속해 있어야 합니다. 기본적으로 .vsct 파일에는 ("Extensibility" / "Custom Command"를 추가한 경우) 한 개의 그룹이 등록되어 있으므로,

<Groups>
    <Group guid="guidMacroCommandPackageCmdSet" id="MyMenuGroup" priority="0x0600">
        <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
    </Group>
</Groups>

위의 "MyMenuGroup"과 연결될 메뉴만 하나 추가해 주면 됩니다.

<Menus>
    <Menu guid="guidMacroCommandPackageCmdSet" id="MyMenuController" priority="0x1000" type="MenuController">
	<Parent guid="guidMacroCommandPackageCmdSet" id="MyMenuGroup" />
	<CommandFlag>DynamicVisibility</CommandFlag>
	<Strings>
	    <ButtonText>MyMacro List</ButtonText>
	</Strings>
    </Menu>
</Menus>

위의 상태까지만 보면, "Tools" 메뉴에 "MyMacro List"라는 이름의 메뉴가 생성되도록 정의되는데 그 메뉴의 타입이 "MenuController" 유형입니다. (이 유형이어야만 자유롭게 자신의 서브 메뉴를 정의할 수 있는 것 같습니다.)

그다음, 서브 메뉴를 담을 그룹을 하나 정의합니다.

<Groups>
    <Group guid="guidMacroCommandPackageCmdSet" id="MyMenuGroup" priority="0x0600">
        <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
    </Group>

    <Group guid="guidMacroCommandPackageCmdSet" id="MySubMenuGroup" priority="0x0600">
        <Parent guid="guidMacroCommandPackageCmdSet" id="MyMenuController" />
    </Group>
</Groups>

당연히 이 서브 메뉴 그룹의 부모는 "Tools" / "MyMacro List" 메뉴가 되어야 하므로 "<Parent />"를 통해 메뉴의 id인 "MyMenuController"를 부모로 지정해 준 것입니다. 그리고 이 서브 메뉴(예제에서는 매크로 명령어들)가 나열되기 위한 기본 버튼(이라 쓰고 메뉴라고 읽음) 하나를 지정해 줍니다.

<Buttons>
    <Button guid="guidMacroCommandPackageCmdSet" id="cmdidMyDynamicStartCommand" priority="0x0100">
    <Parent guid="guidMacroCommandPackageCmdSet" id="MySubMenuGroup" />
    <CommandFlag>DynamicItemStart</CommandFlag>
    <CommandFlag>DynamicVisibility</CommandFlag>
    <CommandFlag>TextChanges</CommandFlag>
    <Strings>
        <ButtonText>MacroSubList</ButtonText>
    </Strings>
    </Button>
</Buttons>

마지막으로, 지금까지 설정했던 노드마다 사용되었던 ID에 대한 심벌 등록을 해줍니다.

<GuidSymbol name="guidMacroCommandPackageCmdSet" value="{6d15ea04-448b-4c65-b70c-cbfaa3c105b7}">
    <IDSymbol name="MyMenuGroup" value="0x1000" />
    <IDSymbol name="MySubMenuGroup" value="0x1500" />
    <IDSymbol name="MyMenuController" value ="0x2000"/>
    <IDSymbol name="cmdidMyDynamicStartCommand" value="0x3002" />
</GuidSymbol>

일단, 여기까지의 .vsct 파일 변경과 함께 지난번의 소스 코드에서 MacroCommand의 생성자를 다음과 같이 아무 일도 안 하는 코드로 변경해서 실행하면,

private MacroCommand(Package package)
{
    if (package == null)
    {
        throw new ArgumentNullException("package");
    }
            
    this.package = package;
}

"Tools" 메뉴에 "MyMacro List" 메뉴가 포함되고, 그 하위로 "MacroSubList" 메뉴가 뜹니다.

dynamic_menu_in_vsix_1.png




우리가 원하는 것은, 바로 "MacroSubList"가 나와야 되는 단계의 메뉴를 동적으로 구성하는 것입니다. 이를 위해 MacroCommand의 생성자를 다음과 같이 변경해 줍니다.

public const int CommandId = 0x3002;  // .vsct에서 지정한 cmdidMyDynamicStartCommand 심벌 값
private DTE2 dte2;

private MacroCommand(Package package)
{
    if (package == null)
    {
        throw new ArgumentNullException("package");
    }

    _repo = new MacroRepository(); // 이 클래스 코드는 나중에 설명을 합니다.
            
    this.package = package;

    OleMenuCommandService commandService = this.ServiceProvider.GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
    if (commandService != null)
    {
        CommandID dynamicItemRootId = new CommandID(CommandSet, (int)CommandId);
        DynamicItemMenuCommand dynamicMenuCommand = new DynamicItemMenuCommand(dynamicItemRootId,
            _repo,
            OnInvokedDynamicItem,
            OnBeforeQueryStatusDynamicItem);

        commandService.AddCommand(dynamicMenuCommand);
    }

    dte2 = (DTE2)this.ServiceProvider.GetService(typeof(DTE));
}

가장 다른 점이 있다면 기존의 코드는 MenuCommand 인스턴스를 생성해 commandService.AddCommand에 건네주었던 반면, 이번에는 별도로 정의한 DynamicItemMenuCommand 타입을 생성해서 전달하고 있습니다. 이 타입은 OleMenuCommand를 상속받은 것으로 다음과 같이 코드를 추가하면 됩니다.

using System;
using System.ComponentModel.Design;
using System.Globalization;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using EnvDTE80;

namespace VSMacro
{
    public class DynamicItemMenuCommand : OleMenuCommand
    {
        MacroRepository _repo;

        public DynamicItemMenuCommand(CommandID rootId, MacroRepository repo,
            EventHandler invokeHandler, EventHandler beforeQueryStatusHandler)
            : base(invokeHandler, null /*changeHandler*/, beforeQueryStatusHandler, rootId)
        {
            _repo = repo;
        }

        public override bool DynamicItemMatch(int cmdId)
        {
            if (_repo.ContainsCmd(cmdId))
            {
                this.MatchedCommandId = cmdId;
                return true;
            }

            return false;
        }
    }
}

DynamicItemMenuCommand가 하는 것은 오로지 Visual Studio가 조회하는 Command ID가 올바른지에 대해서만 DynamicItemMatch 가상 함수를 통해 판단해 주는 것입니다.

여기서부터, 구현이 좀 복잡하고 이해가 다소 직관적이지 않은 부분이 나옵니다. 일단, Visual Studio는 (OleMenuCommand를 상속받은) 메뉴가 있는 경우, 그 메뉴의 생성자에 전달된 마지막 인자(OnBeforeQueryStatusDynamicItem)였던 delegate를 호출합니다. 즉, 다음의 코드가 실행됩니다.

private void OnBeforeQueryStatusDynamicItem(object sender, EventArgs args)
{
    DynamicItemMenuCommand matchedCommand = (DynamicItemMenuCommand)sender;

    matchedCommand.Enabled = true;
    matchedCommand.Visible = true;

    // matchedCommand.MatchedCommandId
}

그런데, 이때 실행되는 matchedCommand의 MatchedCommandId가 수상합니다. 예상으로는 생성자 호출 시에 넘겨줬던 CommandID를 따르는 CommandId(0x3002)여야 할 텐데, Visual Studio는 ("MacroSubList"로 이미 하나 점유하고 있던) 첫 번째 메뉴에 대해 matchedCommand.MatchedCommandId == 0으로 설정해서 호출합니다. 따라서, 동적 메뉴의 첫 번째 항목을 구분하는 Command ID는 0이 되고, 그에 따라 텍스트를 다음과 같은 식으로 변경해 줘야 합니다.

private void OnBeforeQueryStatusDynamicItem(object sender, EventArgs args)
{
    DynamicItemMenuCommand matchedCommand = (DynamicItemMenuCommand)sender;

    matchedCommand.Enabled = true;
    matchedCommand.Visible = true;

    if (matchedCommand.MatchedCommandId == 0)
    {
        matchedCommand.Text = "CMD1";
    }
}

즉, 동적 메뉴라고는 하지만 최소 1개는 이미 포함하고 있는 상태의 메뉴를 구성해야 하는 것입니다.

그다음, Visual Studio는 추가로 메뉴가 있는지에 대해 알아내야 하는데 이를 위해 (OleMenuCommand를 상속받은) DynamicItemMenuCommand로 cmdidMyDynamicStartCommand(0x3002) 값에 +1을 하면서 그 값이 유효한 지에 대해 OleMenuCommand.DynamicItemMatch 메서드를 통해 묻습니다. 따라서 표시해야 할 동적 메뉴가 2개 더 있다면 다음과 같이 구현해 줘야 합니다.

public override bool DynamicItemMatch(int cmdId)
{
    if (cmdId == 0)
    {
        return true; // 가장 첫 번째 메뉴 == 유효
    }

    if (cmdId == (MacroCommand.CommandId + 1))
    {
        this.MatchedCommandId = cmdId;
        return true;  // 두 번째 메뉴 == 유효
    }

    if (cmdId == (MacroCommand.CommandId + 2))
    {
        this.MatchedCommandId = cmdId;
        return true; // 세 번째 메뉴 == 유효
    }

    return false; // 이외의 메뉴 ID들은 유효하지 않음.
}

이쯤에서, 위와 같이 구성하는 것이 다소 번거로우니 동적 메뉴를 위한 구성을 담을 MacroRepository 코드를 다음과 같이 작성합니다.

using System.Collections.Generic;

namespace VSMacro
{
    public class MacroRepository
    {
        List<MacroItem> _macros = new List<MacroItem>();

        public MacroRepository()
        {
            _macros.Add(new MacroItem(0, "CMD1", "__cmd1__"));
            _macros.Add(new MacroItem(MacroCommand.CommandId + 1, "CMD2", "__cmd2__"));
            _macros.Add(new MacroItem(MacroCommand.CommandId + 2, "CMD3", "__cmd3__"));
        }

        public string GetCommandTextByPos(int index)
        {
            if (index != 0)
            {
                index = index - MacroCommand.CommandId;
            }

            return _macros[index].Text;
        }

        public string GetCommandSourceCodeByPos(int index)
        {
            if (index != 0)
            {
                index = index - MacroCommand.CommandId;
            }

            return _macros[index].SourceCode;
        }

        public bool ContainsCmd(int cmdId)
        {
            return _macros.Exists((item) => item.Id == cmdId);
        }
    }

    public class MacroItem
    {
        int _id;
        public int Id
        {
            get { return _id; }
        }

        string _text;
        public string Text
        {
            get { return _text; }
        }

        string _sourceCode;
        public string SourceCode
        {
            get { return _sourceCode; }
        }

        public MacroItem()
        {
        }

        public MacroItem(int id, string text, string sourceCode)
        {
            _id = id;
            _text = text;
            _sourceCode = sourceCode;
        }
    }
}

그럼, DynamicItemMenuCommand.DynamicItemMatch 코드를 다음과 같이 변경할 수 있고,

public override bool DynamicItemMatch(int cmdId)
{
    if (_repo.ContainsCmd(cmdId))
    {
        this.MatchedCommandId = cmdId; // 이때 설정한 MatchedCommandId 값 덕분에,
                                        // 연이어 호출되는 OnBeforeQueryStatusDynamicItem 메서드에서
                                        // 방금 전에 Visual Studio가 조회한 동적 메뉴의 ID가 무엇인지 알 수 있게 됨.
        return true;
    }

    return false;
}

MacroCommand.OnBeforeQueryStatusDynamicItem 메서드는 이렇게 정의할 수 있습니다.

private void OnBeforeQueryStatusDynamicItem(object sender, EventArgs args)
{
    DynamicItemMenuCommand matchedCommand = (DynamicItemMenuCommand)sender;

    matchedCommand.Enabled = true;
    matchedCommand.Visible = true;

    matchedCommand.Text = _repo.GetCommandTextByPos(matchedCommand.MatchedCommandId);

    matchedCommand.MatchedCommandId = 0;
}

위의 코드들을 보시면, 좀 이해가 안 되는 부분이 있습니다. 전통적인 생각으로는, 동적 메뉴를 구성한다면 Menu 객체가 하나 있고 그것의 Children 속성에 개별 메뉴에 해당하는 Menu 인스턴스들이 생성될 것으로 예상하게 됩니다. 가령 다음과 같은 코드를 상상할 수 있습니다.

// Visual Studio 측 (가상 코드)
Menu root = new Menu("MyMacro List");
MenuExtension_AddSubMenu(root);

// 확장 모듈 측
MenuExtension_AddSubMenu(root)
{
    root.Children.Add(new Menu("CMD1"));
    root.Children.Add(new Menu("CMD2"));
    root.Children.Add(new Menu("CMD3"));
}

그런데, Visual Studio는 다음과 같은 식으로 동적 메뉴를 구성하고 있습니다.

// Visual Studio 측 (가상 코드)
Menu root = new Menu("MyMacro List");
Menu subMenu = new Menu("MacroSubList");

OleMenuCommand dynamicMenu = new ExtensionMenu(subMenu);

dynamicMenu.MatchedCommandId = 0;
dynamicMenu.OnBeforeQueryStatusDynamicItem(dynamicMenu, EventArgs.Empty); // 이때 subMenu.Text 속성에 "CMD1"을 설정해야 함.

int startId = dynamicMenu.MatchedCommandId + 1;
while (true)
{
    dynamicMenu.MatchedCommandId = startId;

    if (dynamicMenu.DynamicItemMatch(startId) == false)
    {
        break;
    }

    dynamicMenu.OnBeforeQueryStatusDynamicItem(dynamicMenu, EventArgs.Empty); // 이때 2번째 이후의 동적 메뉴의 문자열을 설정
    startId ++;
}

가만 보면, OleMenuCommand 인스턴스 하나로 동적으로 추가해야 할 모든 메뉴를 처리하고 있는 것입니다. 즉, OnBeforeQueryStatusDynamicItem이나 DynamicItemMatch가 호출될 때의 메뉴 인스턴스는 모두 동일하게/단일한 DynamicItemMenuCommand 인스턴스가 되는 것입니다. (그렇습니다, 처리 절차가 일반적이지 않아서 좀 혼란스럽습니다.)

암튼 저렇게 해서 결국 동적 메뉴로 구성된 항목을 사용자가 선택하면 DynamicItemMenuCommand의 3번째 인자로 넘겨준 OnInvokedDynamicItem 메서드를 호출해 줍니다.

private void OnInvokedDynamicItem(object sender, EventArgs args)  
{  
    DynamicItemMenuCommand invokedCommand = (DynamicItemMenuCommand)sender;  

    // invokedCommand.MatchedCommandId
    // invokedCommand.Text
}  

재미있는 것은, 이 메서드가 호출될 때의 invokedCommand.MatchedCommandId는 해당 메뉴의 존재를 확인하기 위해 DynamicItemMatch를 통해 주고받았던 ID라고 장담할 수 없습니다. 왜냐하면 결국 동적 메뉴를 의미하는 DynamicItemMenuCommand 객체는 1개이기 때문에 그 인스턴스에 현재 어떤 값을 설정하고 있느냐에 따라 달라질 수 있기 때문입니다. 즉, 개발자가 작성한 코드가 어떤 식이냐에 따라 MatchedCommandId가 선택된 메뉴의 ID가 맞을 수도/맞지 않을 수도 있습니다.

사실 Visual Studio는 선택된 동적 메뉴의 MatchedCommandId를 보관하고 있지 않는 것처럼(실제로 보관하고 있지 않는 듯) 행동합니다. 그래서, 첫 번째 메뉴가 선택되었을 때 Visual Studio는 OnInvokedDynamicItem을 다음과 같은 문맥으로 호출해 줍니다.

// Visual Studio 측 (가상 코드)

OleMenuCommand dynamicMenu = ...;

dynamicMenu.MatchedCommandId = 0;
dynamicMenu.OnBeforeQueryStatusDynamicItem(dynamicMenu, EventArgs.Empty);
dynamicMenu.OnInvokedDynamicItem(dynamicMenu, EventArgs.Empty);

즉, 첫 번째 메뉴이기 때문에 MatchedCommandId를 0으로 설정하고 OnBeforeQueryStatusDynamicItem 호출로 상태를 확인한 다음 OnInvokedDynamicItem을 호출하는 것입니다.

동적으로 추가된 두 번째 메뉴부터는 말 그대로 .vsct에 등록했던 cmdidMyDynamicStartCommand(0x3002)의 값에 "+ N"을 하면서 이렇게 호출합니다.

// Visual Studio 측 (가상 코드)
OleMenuCommand dynamicMenu = ...;

// 2번째 메뉴가 선택되었다면,
dynamicMenu.DynamicItemMatch(cmdidMyDynamicStartCommand + 1);           // 호출로 인해 dynamicMenu.MatchedCommandId == cmdidMyDynamicStartCommand + 1로 설정됨
dynamicMenu.OnBeforeQueryStatusDynamicItem(dynamicMenu, EventArgs.Empty); // 호출로 인해 dynamicMenu.MatchedCommandId == 0으로 설정됨
dynamicMenu.DynamicItemMatch(cmdidMyDynamicStartCommand + 1);           // 호출로 인해 다시 dynamicMenu.MatchedCommandId == cmdidMyDynamicStartCommand + 1로 설정됨
dynamicMenu.OnInvokedDynamicItem(dynamicMenu, EventArgs.Empty);         // 결국 dynamicMenu.MatchedCommandId == cmdidMyDynamicStartCommand + 1 값으로 호출됨

// 3번째 메뉴가 선택되었다면,
dynamicMenu.DynamicItemMatch(cmdidMyDynamicStartCommand + 2);           // 호출로 인해 dynamicMenu.MatchedCommandId == cmdidMyDynamicStartCommand + 2로 설정됨
dynamicMenu.OnBeforeQueryStatusDynamicItem(dynamicMenu, EventArgs.Empty); // 호출로 인해 dynamicMenu.MatchedCommandId == 0으로 설정됨
dynamicMenu.DynamicItemMatch(cmdidMyDynamicStartCommand + 2);           // 호출로 인해 다시 dynamicMenu.MatchedCommandId == cmdidMyDynamicStartCommand + 1로 설정됨
dynamicMenu.OnInvokedDynamicItem(dynamicMenu, EventArgs.Empty);         // 결국 dynamicMenu.MatchedCommandId == cmdidMyDynamicStartCommand + 1 값으로 호출됨

저 상황을 보고, DynamicItemMatch, OnBeforeQueryStatusDynamicItem의 메서드 구현을 다시 보면 왜 MatchedCommandId의 값에 0이나 cmdId를 명시적으로 설정해 주는 코드가 들어가야 하는지 이해할 수 있습니다.




위와 같은 코드로 Visual Studio를 실행하면 다음과 같은 화면을 볼 수 있고,

dynamic_menu_in_vsix_2.png

각각의 메뉴를 선택하면, MacroItem 객체에 설정했던 SourceCode 속성의 문자열이 메시지 상자로 뜨는 것을 확인할 수 있습니다.

(이 글의 첨부 파일은 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/13/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 31  32  33  34  35  36  37  38  39  40  41  [42]  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12571정성태3/21/20218361오류 유형: 706. WSL 2 기반으로 "Enable Kubernetes" 활성화 시 초기화 실패 [1]
12570정성태3/19/202112678개발 환경 구성: 555. openssl - CA로부터 인증받은 새로운 인증서를 생성하는 방법
12569정성태3/18/202111554개발 환경 구성: 554. WSL 인스턴스 export/import 방법 및 단축 아이콘 설정 방법
12568정성태3/18/20217257오류 유형: 705. C# 빌드 - Couldn't process file ... due to its being in the Internet or Restricted zone or having the mark of the web on the file.
12567정성태3/17/20218583개발 환경 구성: 553. Docker Desktop for Windows를 위한 k8s 대시보드 활성화 [1]
12566정성태3/17/20218923개발 환경 구성: 552. Kubernetes - kube-apiserver와 REST API 통신하는 방법 (Docker Desktop for Windows 환경)
12565정성태3/17/20216438오류 유형: 704. curl.exe 실행 시 dll not found 오류
12564정성태3/16/20216936VS.NET IDE: 160. 새 프로젝트 창에 C++/CLI 프로젝트 템플릿이 없는 경우
12563정성태3/16/20218885개발 환경 구성: 551. C# - JIRA REST API 사용 정리 (3) jira-oauth-cli 도구를 이용한 키 관리
12562정성태3/15/20219978개발 환경 구성: 550. C# - JIRA REST API 사용 정리 (2) JIRA OAuth 토큰으로 API 사용하는 방법파일 다운로드1
12561정성태3/12/20218604VS.NET IDE: 159. Visual Studio에서 개행(\n, \r) 등의 제어 문자를 치환하는 방법 - 정규 표현식 사용
12560정성태3/11/20219950개발 환경 구성: 549. ssh-keygen으로 생성한 개인키/공개키 파일을 각각 PKCS8/PEM 형식으로 변환하는 방법
12559정성태3/11/20219335.NET Framework: 1028. 닷넷 5 환경의 Web API에 OpenAPI 적용을 위한 NSwag 또는 Swashbuckle 패키지 사용 [2]파일 다운로드1
12558정성태3/10/20218859Windows: 192. Power Automate Desktop (Preview) 소개 - Bitvise SSH Client 제어 [1]
12557정성태3/10/20217497Windows: 191. 탐색기의 보안 탭에 있는 "Object name" 경로에 LEFT-TO-RIGHT EMBEDDING 제어 문자가 포함되는 문제
12556정성태3/9/20216784오류 유형: 703. PowerShell ISE의 Debug / Toggle Breakpoint 메뉴가 비활성 상태인 경우
12555정성태3/8/20218785Windows: 190. C# - 레지스트리에 등록된 DigitalProductId로부터 라이선스 키(Product Key)를 알아내는 방법파일 다운로드2
12554정성태3/8/20218620.NET Framework: 1027. 닷넷 응용 프로그램을 위한 PDB 옵션 - full, pdbonly, portable, embedded
12553정성태3/5/20219094개발 환경 구성: 548. 기존 .NET Framework 프로젝트를 .NET Core/5+ 용으로 변환해 주는 upgrade-assistant, try-convert 도구 소개 [4]
12552정성태3/5/20218360개발 환경 구성: 547. github workflow/actions에서 Visual Studio Marketplace 패키지 등록하는 방법
12551정성태3/5/20217265오류 유형: 702. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly. (2)
12550정성태3/5/20216976오류 유형: 701. Live Share 1.0.3713.0 버전을 1.0.3884.0으로 업데이트 이후 ContactServiceModelPackage 오류 발생하는 문제
12549정성태3/4/20217419오류 유형: 700. VsixPublisher를 이용한 등록 시 다양한 오류 유형 해결책
12548정성태3/4/20218189개발 환경 구성: 546. github workflow/actions에서 nuget 패키지 등록하는 방법
12547정성태3/3/20218681오류 유형: 699. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly.
12546정성태3/3/20218332개발 환경 구성: 545. github workflow/actions에서 빌드시 snk 파일 다루는 방법 - Encrypted secrets
... 31  32  33  34  35  36  37  38  39  40  41  [42]  43  44  45  ...