처음엔 괜찮았는데 어느 시점에 VS의 디자이너에서 form 로드 오류가 발생합니다.
B도 로드 오류가 발생합니다. A는 멀쩡합니다.
컨트롤 A의 생성자/OnLoad 이벤트 내에서 DB 접근 등의 행위가 있는데, A가 자식 컨트롤로 다른 컨트롤에 포함될 경우 Component.DesignMode가 False가 된 것이 그 원인으로 보입니다.
여기에서 질문이 크게 두 가지입니다.
처음 컨트롤 A의 생성자/OnLoad 이벤트에서 DesignTime에 대한 DB 접근 코드 분리를 하지 않았는데도 A는 멀쩡했던 이유
OnLoad(){
DBConnectionFunction();
}
// 이와 같이 분리하지 않았는데 A에서는 오류가 발생하지 않았습니다.
사고의 흐름을 보면, 별도의 DesignTime 분리 코드를 넣지 않았기 때문에 A에 대한 디자이너를 열었을 때 A의 생성자/OnLoad 이벤트가 호출되며 DB 연결 함수가 실행, 오류가 발생하면서 A에 대한 디자이너에서도 B, 폼1의 디자이너와 마찬가지로 오류가 나야 한다는 것이 제 생각입니다.
DesignTime을 구분하는 더 나은 방법
위에서 언급한 것과 같이 원인을 파악하고 난 후, 아래 코드로 DesignTime에 실행 되어서는 안 되는 코드를 분리, DesignTime에서 오류가 발생하지 않도록 하였습니다.
if (System.Reflection.GetEntryAssembly() == null)
return;
DBConnectionFunction();
더 찾아보니, DesignTime을 판별하는 방법으로 DesignMode 프로퍼티와 위 코드 외에 LicenseManager.UsageMode가 존재하는 것을 알았습니다.
DesignMode의 경우 제가 겪었던 문제처럼 자식 컨트롤로 들어가는 경우에 값이 false가 되어 상황에 따라 적절하지 않게 동작할 수 있어서 제외한다고 하면, 결국 남는건 reflection과 licenseManager 두 방법인데 둘 중 어떤 것이 더 적절한지 잘 모르겠습니다.
Visual studio에서 winform 개발 시 디자이너에 대한 제가 이해하고 있는 내용 기준으로 설명드려봅니다.
혹여나 내용에 잘못된 부분이 있으면 지적 부탁드립니다.
Winform 개발 시 Visual studio에서 디자이너가 표시되는 방식이 우선 cs 파일의 첫번째 클래스가 Form 클래스 또는 UserControl 클래스를 상속 받고 있는지 확인합니다. 이 여부에 따라 Visual studio 솔루션 탐색기에서 표시되는 여부가 달라집니다.
만약 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 이벤트에 특정 로직을 구현했을 때 디자이너에서 호출이 되는 것을 확인해볼 수 있습니다.
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"를 사용할 수 있을 것 같습니다.