Slate(가칭), 작게 시작한 WPF 도구

:link:LinkedIn 소개 에도 올렸던 라이브러리입니다.

원래는 클론 프로젝트를 완성하려 했지만,
리소스 부족으로 마무리를 짓기는 어려울 것 같고,
대신 라이브러리를 좀 더 발전시키고 싶다는 생각이 들어
이렇게 Slog로 남겨봅니다.


WPF를 하다 보면, 한 번쯤은 이런 생각이 들죠.

“MVVM이 너무 어렵다…”
“XAML 진짜 보기 싫다…”
“화면 전환 좀 깔끔하게 하고 싶은데 왜 이렇게 복잡하지?”

저도 비슷한 고민을 했고, 그래서 직접 만들어보기로 했습니다.

:hammer_and_wrench: Slate은 WPF 개발을 조금 더 유연하게,
그리고 MVVM이나 XAML로부터 오는 부담을 덜어주는 걸 목표로 시작한 라이브러리입니다.


:bulb: 왜 만들었나요?

작은 프로젝트를 빠르게 만들고 싶을 때,
초반 진입장벽이 너무 크다고 느꼈습니다.

  • 화면 전환은 왜 이리 번거롭고,
  • ViewModel은 꼭 이렇게 얽혀 있어야 하고,
  • UI는 왜 꼭 XAML로만 짜야 할까?

이런 "작은 불편함"들이 쌓여 결국 개발 속도를 떨어뜨린다고 느꼈습니다.


:sparkles: Slate의 방향

  • :file_folder: 폴더 트리 기반 화면 전환
    네임스페이스 구조 그대로 View를 찾습니다.
    예: Pages.Home/Pages/Home/Content.cs
  • :speech_balloon: XAML 없이, C#만으로 UI 구성 가능
    DSL 스타일로 선언적인 UI 코드를 C#으로 직접 작성할 수 있게 했습니다.
    복잡한 마크업 대신, 더 명확하고 유연하게 표현할 수 있습니다.

:pushpin: 아직 부족한 점은 많습니다

Slate은 아직 이름도 제대로 정해지지 않았고, 부족한 부분도 많습니다.
하지만 무엇을 추구하는지는 샘플 코드에 담아보았습니다.

이 Slog를 통해 발전해 나가는 모습,
그리고 오가는 의견 속에서 어떤 방향으로 흘러갈 수 있을지 함께 나누고 싶습니다.


:link: 샘플 프로젝트

:link: GitHub: Battlent.WPF

사용해보시고, 괜찮다 싶으시면 Star​:star2: 하나 눌러주세요 :slight_smile:

완성된 건 아니지만, 시작은 했습니다.
천천히 나아가 보려고요.

13개의 좋아요

프로젝트 구성


기본 WPF 프로젝트와 다르게 Window를 없앴습니다.
이유는 Window를 보면 그것부터 해야만할 것 같은 생각을 지우기 위해서였죠.
윈도우가 없는 대신 이것은 지켜주셔야합니다.
Slate는 *Region 을 이용합니다.
그렇기 때문에 어느 Region에 넣을 지 설정합니다. (아래 소스 참고)

Bootstrapper를 통한 프로젝트 시작

// App.xaml.cs
namespace SlateLab
{
    public class SlateBootstrapper : Bootstrapper
    {
        // RegisterComponent는 전환하고자 하는 Layout 그리고 시작 Layout은 무조건 등록해야합니다.
        // Layout(Content)는 무조건 어느 Region에 속하는지 기입해줘야합니다.
        protected override void Register(IContainerRegistry containerRegistry)
        {
            base.Register (containerRegistry);
            containerRegistry.RegisterComponent<Content> ();

        }
    }

    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup (e);
            var bootstrapper = new SlateBootstrapper ()
                                  .UseMarkupHotReload(this)  // XAML을 사용하지 않을 경우 핫리로드를 위해서 
 필요                             .StartLayout<Content>();   // 시작 화면 설정
            bootstrapper.Run (); 
        }
    }
}

GlobalUsing의 사용(사용안해도됨)

// GlobalUsing.cs
global using Slate;                 // 기본
global using Slate.WPF;             // 기본
global using Slate.WPF.Markup;      // C#을 통한 마크업 라이브러리
global using MarkupChain.WPF;       // C#을 통한 마크업 라이브러리
global using System.Windows;
global using System.Windows.Controls;

시작 컴포넌트

using System.Windows.Media;

namespace SlateLab
{
    public class Content : Component
    {
        public override void RegionAttached(object argu)
        {
            RegionManager.Attach ("Root", this);
        }
        protected override Visual Build()
            => new Grid ()
            {
            }
            .Size(500, 300);
    }
}

왜 Root일까?


기본으로 만들어져있는 윈도우의 경우 설정 된 RegionName이 Root이기 때문입니다.

시작 컴포넌트는 꼭 클래스여야만할까? :x: NO

UserControl로 만들고 인터페이스 상속만 지정 후
영역만 지정해주면됩니다.

namespace SlateLab
{
    /// <summary>
    /// Content.xaml에 대한 상호 작용 논리
    /// </summary>
    public partial class Content : UserControl, IShellComponent
    {
        public Content()
        {
            InitializeComponent ();
        }

        public void RegionAttached(object argu = null)
        {
            RegionManager.Attach ("Root", this);
        }
    }
}


:large_orange_diamond: 리전(Region)이란?

**WPF 화면(UI)**을 동적으로 구성하기 위한 플러그인 구조의 UI 슬롯이라고 이해하면 좋습니다.

  • 말하자면, 특정 UI 영역을 "이곳은 동적으로 View를 꽂을 수 있는 자리"로 선언해놓습니다.
  • 그 자리에 ViewModel이나 View를 나중에 “등록” 또는 "교체"하면서, 화면을 모듈화하고 느슨하게 결합하는 게 목적입니다.

출처 : Chatgpt

4개의 좋아요


오오… 뭔가 신기합니다!!! (찡긋)

3개의 좋아요

:slight_smile: 기서 리전컨트롤 경험해보시면 입이쩌억 벌어지십니다…:slight_smile:

2개의 좋아요


마크업 느낌으로!

4개의 좋아요

Component의 프로퍼티 변수 사용하기

1. Bind() 방식 — 개발 완료

기존 WPF 개발자들이 XAML 바인딩이 익숙해서 어색하겠지만… Maui.MarkupBind() 메서드를 활용해 적용하였습니다.

public partial class Content : Component
{
    [ObservableProperty] int count = 0;

    public override void RegionAttached(object argu)
    {
        base.RegionAttached(argu);
        RegionManager.Attach("Root", this);
    }

    protected override Visual Build()
        => new VStack()
        {
            Children =
            {
                new Label()
                    .Bind(Label.ContentProperty, x => x.Count, this),  // Count 프로퍼티를 ContentProperty에 바인딩
                new Button()
                    .Size(100, 50)
                    .Content("플러스!!!")
                    .OnTapped(() => { Count++; }),
                new Button()
                    .Size(100, 50)
                    .Content("마이너스!!!")
                    .OnTapped(() => { Count--; })
            }
        }.Size(500, 500);
}
  • Bind()를 사용해 Label.ContentPropertyCount 프로퍼티를 바인딩합니다.
  • 버튼 클릭 시 Count 값을 증감시키면 UI가 자동 갱신됩니다.
  • 기존 XAML 바인딩 경험자에게 친숙한 패턴입니다.

  1. State 방식 — 개발 진행 중
    현재 개발 중인 State<T>를 활용한 예제입니다. State<T>는 내부 값 변경 시 UI가 자동 업데이트되는 상태 관리 구조입니다.
public partial class Content : Component
{
    readonly State<int> Count = 0;

    public override void RegionAttached(object argu)
    {
        base.RegionAttached(argu);
        RegionManager.Attach("Root", this);
    }

    protected override Visual Build()
        => new VStack()
        {
            Children =
            {
                new Label()
                    .Content(Count),  // State<int> 타입 Count 바인딩
                new Button()
                    .Size(100, 50)
                    .Content("플러스!!!")
                    .OnTapped(() => { Count.Value++; }),
                new Button()
                    .Size(100, 50)
                    .Content("마이너스!!!")
                    .OnTapped(() => { Count.Value--; })
            }
        }.Size(500, 500);
}
  • State<T> 타입을 선언하여 상태 변화를 관리합니다.
  • Count.Value를 변경하면 UI가 자동으로 갱신됩니다.
  • Bind() 방식과 달리, State<T>가 자체적으로 변경 알림 기능을 내장합니다.
  • 아직 개발 중이므로 개선 및 확장이 예정되어 있습니다.

두 가지 방식을 비교해보면서, 본인의 프로젝트에 맞는 방식을 선택해보세요!
필요하면 State<T> 쪽을 좀 더 발전시켜 MVVM 패턴에 적합하도록 개선할 계획입니다.

2개의 좋아요