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

비밀번호

댓글 작성자
 




... [91]  92  93  94  95  96  97  98  99  100  101  102  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11371정성태11/25/201713473오류 유형: 432. Hyper-V 가상 스위치 생성 시 Failed to connect Ethernet switch port 0x80070002 오류 발생
11370정성태11/25/201713056오류 유형: 431. Hyper-V의 Virtual Switch 생성 시 "External network" 목록에 특정 네트워크 어댑터 항목이 없는 경우
11369정성태11/25/201715383사물인터넷: 12. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 키보드 및 마우스로 쓰는 방법 (절대 좌표, 상대 좌표, 휠) [1]
11368정성태11/25/201720531.NET Framework: 699. UDP 브로드캐스트 주소 255.255.255.255와 192.168.0.255의 차이점과 이를 고려한 C# UDP 서버/클라이언트 예제 [2]파일 다운로드1
11367정성태11/25/201720647개발 환경 구성: 337. 윈도우 운영체제의 route 명령어 사용법
11366정성태11/25/201712471오류 유형: 430. 이벤트 로그 - Cryptographic Services failed while processing the OnIdentity() call in the System Writer Object.
11365정성태11/25/201714725오류 유형: 429. 이벤트 로그 - User Policy could not be updated successfully
11364정성태11/24/201715762사물인터넷: 11. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스로 쓰는 방법 (절대 좌표) [2]
11363정성태11/23/201715474사물인터넷: 10. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스 + 키보드로 쓰는 방법 (두 번째 이야기)
11362정성태11/22/201713117오류 유형: 428. 윈도우 업데이트 KB4048953 - 0x800705b4 [2]
11361정성태11/22/201715722오류 유형: 427. 이벤트 로그 - Filter Manager failed to attach to volume '\Device\HarddiskVolume??' 0xC03A001C
11360정성태11/22/201715478오류 유형: 426. 이벤트 로그 - The kernel power manager has initiated a shutdown transition.
11359정성태11/16/201714848오류 유형: 425. 윈도우 10 Version 1709 (OS Build 16299.64) 업그레이드 시 발생한 문제 2가지
11358정성태11/15/201719151사물인터넷: 9. Visual Studio 2017에서 Raspberry Pi C++ 응용 프로그램 제작 [1]
11357정성태11/15/201719919개발 환경 구성: 336. 윈도우 10 Bash 쉘에서 C++ 컴파일하는 방법
11356정성태11/15/201721272사물인터넷: 8. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스 + 키보드로 쓰는 방법 [4]
11355정성태11/15/201717783사물인터넷: 7. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스로 쓰는 방법 [2]파일 다운로드2
11354정성태11/14/201721067사물인터넷: 6. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 키보드로 쓰는 방법 [8]
11353정성태11/14/201718809사물인터넷: 5. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 이더넷 카드로 쓰는 방법 [1]
11352정성태11/14/201714457사물인터넷: 4. Samba를 이용해 윈도우와 Raspberry Pi간의 파일 교환 [1]
11351정성태11/7/201717498.NET Framework: 698. C# 컴파일러 대신 직접 구현하는 비동기(async/await) 코드 [6]파일 다운로드1
11350정성태11/1/201713741디버깅 기술: 108. windbg 분석 사례 - Redis 서버로의 호출을 기다리면서 hang 현상 발생
11349정성태10/31/201713627디버깅 기술: 107. windbg - x64 SOS 확장의 !clrstack 명령어가 출력하는 Child SP 값의 의미 [1]파일 다운로드1
11348정성태10/31/201711067디버깅 기술: 106. windbg - x64 역어셈블 코드에서 닷넷 메서드 호출의 인자를 확인하는 방법
11347정성태10/28/201714576오류 유형: 424. Visual Studio - "클래스 다이어그램 보기" 시 "작업을 완료할 수 없습니다. 해당 인터페이스를 지원하지 않습니다." 오류 발생
11346정성태10/25/201710962오류 유형: 423. Windows Server 2003 - The client-side extension could not remove user policy settings for 'Default Domain Policy {...}' (0x8007000d)
... [91]  92  93  94  95  96  97  98  99  100  101  102  103  104  105  ...