Design Mode 확인 시 어떤게 좋을까요?

최근에 프로젝트를 진행하다 이런 일을 겪었습니다.

사용자 정의 컨트롤 A
사용자 정의 컨트롤 B
폼 1

A가 B의 자식 컨트롤로 포함되어 있습니다.
B가 폼1의 자식 컨트롤로 포함되어있습니다.

처음엔 괜찮았는데 어느 시점에 VS의 디자이너에서 form 로드 오류가 발생합니다.
B도 로드 오류가 발생합니다. A는 멀쩡합니다.

컨트롤 A의 생성자/OnLoad 이벤트 내에서 DB 접근 등의 행위가 있는데, A가 자식 컨트롤로 다른 컨트롤에 포함될 경우 Component.DesignMode가 False가 된 것이 그 원인으로 보입니다.

여기에서 질문이 크게 두 가지입니다.

  1. 처음 컨트롤 A의 생성자/OnLoad 이벤트에서 DesignTime에 대한 DB 접근 코드 분리를 하지 않았는데도 A는 멀쩡했던 이유
OnLoad(){
    DBConnectionFunction();
}
// 이와 같이 분리하지 않았는데 A에서는 오류가 발생하지 않았습니다.

사고의 흐름을 보면, 별도의 DesignTime 분리 코드를 넣지 않았기 때문에 A에 대한 디자이너를 열었을 때 A의 생성자/OnLoad 이벤트가 호출되며 DB 연결 함수가 실행, 오류가 발생하면서 A에 대한 디자이너에서도 B, 폼1의 디자이너와 마찬가지로 오류가 나야 한다는 것이 제 생각입니다.

  1. DesignTime을 구분하는 더 나은 방법

위에서 언급한 것과 같이 원인을 파악하고 난 후, 아래 코드로 DesignTime에 실행 되어서는 안 되는 코드를 분리, DesignTime에서 오류가 발생하지 않도록 하였습니다.

if (System.Reflection.GetEntryAssembly() == null)
    return;
DBConnectionFunction();

더 찾아보니, DesignTime을 판별하는 방법으로 DesignMode 프로퍼티와 위 코드 외에 LicenseManager.UsageMode가 존재하는 것을 알았습니다.

DesignMode의 경우 제가 겪었던 문제처럼 자식 컨트롤로 들어가는 경우에 값이 false가 되어 상황에 따라 적절하지 않게 동작할 수 있어서 제외한다고 하면, 결국 남는건 reflectionlicenseManager 두 방법인데 둘 중 어떤 것이 더 적절한지 잘 모르겠습니다.

  1. 그 외로 이러한 문제를 겪을 때 해결하는 방식이 있는지?

정도 질문 드리고 싶습니다.

2010년대 들어서는 WinForms 개발은 안 하고 있지만, 대략 아래와 같은 코드가 잘 동작했던 걸로 기억합니다.

using System.Diagnostics;

...

private static bool? _isDesignTime;

public static IsDesignTime 
{
    get 
    {
        if (_isDesignTime == null)
        {         
            _isDesignTime = Process.GetCurrentProcess().ProcessName == "devenv";
        }

        return _isDesignTime.Value;
    }
}

유틸리티 클래스에 넣어 사용하시면 됩니다.

(요즘 Visual Studio에서는 WinForms 디자이너의 호스팅 방식이 바뀌어서, 이 코드가 제대로 동작할지는 잘 모르겠네요. :rofl::rofl:)

3개의 좋아요

Visual studio에서 winform 개발 시 디자이너에 대한 제가 이해하고 있는 내용 기준으로 설명드려봅니다.

혹여나 내용에 잘못된 부분이 있으면 지적 부탁드립니다.


Winform 개발 시 Visual studio에서 디자이너가 표시되는 방식이 우선 cs 파일의 첫번째 클래스가 Form 클래스 또는 UserControl 클래스를 상속 받고 있는지 확인합니다. 이 여부에 따라 Visual studio 솔루션 탐색기에서 표시되는 여부가 달라집니다.

첫번째 사진 처럼 솔루션 탐색기에 아이콘이 표시가되어야 디자이너 보기 (Shift + F7) 버튼이 활성화 됩니다.

이 디자이너 보기를 눌렀을 때 Visual studio가 디자이너를 표시하는 방식이 InitializeComponent() 메서드가 구현되어있는지 확인하고 구현되어 있다면 해당 코드를 정적 분석하여 각 컨트롤을 디자이너에 표시해줍니다.

  • InitializeComponent에 MessageBox를 넣어도 디자이너에서는 호출이 안되고 실제 애플리케이션을 실행하면은 호출이 됩니다.

만약 InitializeComponent가 구현되어 있지 않다면 디자이너를 갔을 때 아무런 표시가 되어있지 않습니다.
테스트로 Winform 프로젝트 생성 후 디자이너에서 아무런 컨트롤을 배치한 다음, InitializeComponent 메서드 명칭을 바꾸고 다시 디자이너를 보면 아무런 표시가 되어있지 않지만 애플리케이션을 실행하면 정상적으로 각 컨트롤이 표시됩니다.
추가로 생성자에서 InitializeComponent 호출을 제거해도 디자이너에 들어가면 각 컨트롤들이 정상적으로 보이는 것을 확인해볼 수 있습니다.

한가지 주의할 점은 디자이너에 각 컨트롤을 배치하기 전에 해당 클래스의 부모 클래스 생성자가 호출됩니다.

만약 부모 클래스의 생성자에 InitializeComponent 메서드가 있다면 호출되면서 자식 클래스의 디자이너에 Inherited Controls로 같이 표시됩니다.

그리고 생성자 또는 InitializeComponent에서 특정 로직 또는 이벤트 구독 로직이 있다면, 각 컨트롤이 구성될 때 호출이 됩니다.

대표적으로 디자이너에서 많이 볼 수 있는 것이 View layer에서 공통 로직을 추상화하기 위해 abstract 키워드로 Form 클래스를 정의했을 경우 디자이너에 표시되지 않던 문제가 바로 부모 클래스 생성자를 호출하기 때문입니다.

  • abstract 클래스는 인스턴스화할 수 없기 때문입니다.

그 다음으로 앞서 얘기했듯이 실제 부모 클래스 생성자와 각 이벤트가 호출되기 때문에 특정 로직이 들어가있으면 간혹 디자이너에 들어갔는데 코드가 실행되는 문제가 발생하는 것이 위와 같은 이유들 때문입니다.

테스트 코드로 아래와 같이 작성한 후 디자이너에 들어가보면 실제 MessageBox가 표시되는 것을 확인할 수 있습니다.

//BaseForm.cs
public class BaseForm : Form
{
    public BaseForm()
    {
        MessageBox.Show("Ctor Test");
        Load += (sender, e) => MessageBox.Show("Load Test");
        Shown += (sender, e) => MessageBox.Show("Shown Test");
    }
}

//MainForm.cs
public partial class MainForm : BaseForm
{
    public MainForm()
    {
        InitializeComponent();
    }
}

위와 같은 내용들은 Form와 UserControl 둘다 동일하게 발생합니다.
(컨트롤을 상속 했을 때에 대한 내용)


질문 주신 내용은 컨트롤을 상속했을 때에 대한 내용 보다는 각 컨트롤이 디자이너에서 배치됐을 때에 발생한 문제 같습니다.

  • B 컨트롤 디자이너에서 A 컨트롤 배치
  • Form에서 B 컨트롤 배치

Visual studio 디자이너에서 각 컨트롤을 배치하게 되면 Viausl studio가 자동적으로 InitializeComponent에 각 컨트롤의 정보가 추가됩니다.

테스트를 해볼 수 있는 것이 cs 파일을 하나 만들고 Form을 상속받으면 디자이너가 활성화 되는데 이 때 디자이너에서 컨트롤을 추가해보면 InitializeComponent 메서드가 생성되는걸 확인할 수 있습니다.

// 일반 클래스 파일을 만들고 Form을 상속
public class Foo : Form
{
    private Button button1;

    public Foo()
    {
        
    }

    // 디자이너에서 버튼 배치 시 자동 생성
    private void InitializeComponent()
    {
        this.button1 = new System.Windows.Forms.Button();
        this.SuspendLayout();
        // 
        // button1
        // 
        this.button1.Location = new System.Drawing.Point(89, 84);
        this.button1.Name = "button1";
        this.button1.Size = new System.Drawing.Size(75, 23);
        this.button1.TabIndex = 0;
        this.button1.Text = "button1";
        this.button1.UseVisualStyleBackColor = true;
        // 
        // Foo
        // 
        this.ClientSize = new System.Drawing.Size(284, 261);
        this.Controls.Add(this.button1);
        this.Name = "Foo";
        this.ResumeLayout(false);

    }
}

InitializeComponent에 디자이너에서 배치한 각 컨트롤이 표시되는데 사용자가 정의한 UserControl 또한 이 InitializeComponent에 포함됩니다.

Visual studio에서 디자이너를 표시할 때 InitializeComponent를 정적 코드 분석한 다음 디자이너에 표시를 해준다고 했는데 이 때 질문 주신 내용에 대한 문제가 발생합니다.

Visual studio에서 InitializeComponent를 분석한 후 각 컨트롤을 배치할 때 컨트롤 또한 인스턴스화를 진행하고 각 이벤트가 호출됩니다.

아래와 같은 코드로 실제 디자이너에 들어가보면 MessageBox가 표시되는 것을 확인할 수 있습니다.

partial class Form1
{
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    ...

    private void InitializeComponent()
    {
        // Visual studio에서 정적 분석 후 TestButton 클래스를 인스턴스화 진행
        this.button1 = new TestButton();
        this.SuspendLayout();
        // 
        // button1
        // 
        ...
    }

    #endregion

    // 생성자 호출 테스트를 위한 Button 재정의
    private TestButton button1;

    class TestButton : Button
    {
        public TestButton()
        {
            MessageBox.Show("Test!");
        }
    }
}

UserControl 또한 생성자 또는 Load 이벤트에 특정 로직을 구현했을 때 디자이너에서 호출이 되는 것을 확인해볼 수 있습니다.

// UserControl1.cs
public partial class UserControl1 : UserControl
{
    public UserControl1()
    {
        InitializeComponent();
        MessageBox.Show("UC Ctor");
    }

    private void UserControl1_Load(object sender, EventArgs e)
    {
        MessageBox.Show("UC Load");
    }
}

//Form.cs
partial class Form1
{
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    ...

    private void InitializeComponent()
    {
        this.button1 = new TestForm.Form1.TestButton();
        this.userControl11 = new TestForm.UserControl1();
        ...

    private TestButton button1;
    private UserControl1 userControl11;
    ...
}


다음과 같은 이유로 A 컨트롤은 정상적으로 디자이너가 표시되고 그 외 컨트롤은 실패했을 것으로 생각됩니다.

  1. A UserControl의 생성자 또는 OnLoad에 특정 로직 구현
  2. A UserControl의 디자이너에서는 다른 컨트롤의 의존성이 없음, A UserControl의 InitializeComponent만으로 디자이너 활성화 :green_circle:
  3. B UserControl의 디자이너에 A UserControl을 배치
  4. B UserControl의 디자이너 활성화 시 InitializeComponent에 A UserControl이 존재, A UserControl을 인스턴스화 하다가 실패하여 디자이너 에러 발생 :red_circle:
  5. Form의 디자이너에 B UserControl을 배치
  6. Form의 디자이너 활성화 시 InitializeComponent에 B UserControl이 존재, B UserControl을 인스턴스화, B가 생성되면서 B의 InitializeComponent 호출, (4)번과 동일 증상 발생 :red_circle:

제가 알고있는 디자인 타임을 확인하는 방법은 총 3가지이며, 이렇게 디자이너가 표시가 안되는 문제는 코드에서 런타임일 때만 코드가 동작하도록 분기하는 방법밖에 없습니다.

디자인 타임을 확인할 수 있는 방법으로는 아래와 같습니다.

  1. Component.DegisnMode 속성 사용
  2. LicenseManager.UsageMode 정적 속성 사용
  3. al6uiz님이 말씀해주신 현재 프로세스 확인 (devenv)

Component.DegisnMode

해당 속성은 생성자에서 디자인 타임인지 확인이 불가능합니다.

private ISite site;
...
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
protected bool DesignMode => site?.DesignMode ?? false;

DegisnMode의 getter를 확인해보면 site 필드를 사용하는데 이 필드가 생성자에서는 할당이 되어있지 않은 상태이고 Load, Shown 이벤트에서는 확인이 가능합니다.

LicenseManager.UsageMode

만약 생성자에서 확인이 필요하다면 LicenseManager.UsageMode를 이용하여 LicenseManager.UsageMode.Designtime 값을 확인할 수 있지만 Form의 Shown 이벤트와 UserControl의 Load 이벤트에서는 Runtime으로 표시가 되는데 아직 정확한 이유는 확인하지 못했습니다.

//BaseForm.cs
public class BaseForm : Form
{
    public BaseForm()
    { 
        // MainForm에서 디자이너 활성화 시

        // 생성자에서 Designtime으로 반환
        MessageBox.Show($"Constructor : {LicenseManager.UsageMode}");
              
        //  - Load -> Designtime으로 반환
        //  - Shown -> Runtime으로 반환        
        Load += (sender, e) => MessageBox.Show($"Load : {LicenseManager.UsageMode}");
        Shown += (sender, e) => MessageBox.Show($"Shown : {LicenseManager.UsageMode}");

        // 런타임 실행 시 전부 Runtime으로 반환
    }
}

//MainForm.cs
public partial class MainForm : BaseForm
{
    public MainForm()
    {
        InitializeComponent();
    }
}

Process.GetCurrentProcess().ProcessName

이 방법은 visual studio의 프로세스가 devenv.exe로 실행되는데 이 프로세스가 각 로직을 호출합니다.

MessageBox가 표시된 것을 spy++로 확인해보면 devenv.exe인 것을 확인해볼 수 있습니다.


작성해주신 설계대로 테스트를 진행했을 때 A UserControl을 Form 또는 B UserControl에 바로 배치하면 A UserControl의 Load 이벤트에서 Component.DegisnMode 값이 True로 반환이 되지만 A → B → Form 순으로 배치했을 때 Form 디자이너를 활성화하면 A UserControl Load 이벤트에서 Component.DegisnMode 값이 False로 반환이 되네요. (B UserControl 디자이너에서는 True로 반환됩니다.

  • 아마 B의 생성자가 호출이되고 InitializeComponent에서 또 A의 생성자가 호출이되면서 발생한 문제 같은데 이 부분은 확인을 해봐야될 것 같습니다.

만약 특정 로직이 OnLoad 이벤트에서만 동작해야된다면 LicenseManager.UsageMode사용도 어려워 보입니다.

남은 방법으로는 Process.GetCurrentProcess().ProcessName == "devenv"를 사용할 수 있을 것 같습니다.

8개의 좋아요

너무 멋진 글입니다.
커뮤니티가 왜 존재하는지 증명하는 것 같습니다.
정성과 수고에 박수를 보냅니다.

3개의 좋아요

상세히 설명해주셔서 감사합니다, 덕분에 의문이 풀렸습니다!

3개의 좋아요