특정한 속성의 집합을 자동으로 클래스화할 수 있을까요?

wpf에서 mvvm 패턴을 이용해 토이 프로젝트 느낌으로 프로그램을 개발하고 있는데, 계층 구조의 메뉴를 하드코딩하지 않고 다른 방법으로 해결하면 좋겠다는 생각이 들었습니다. 기존에는 생성자에 하위 메뉴나 상위 메뉴를 넣는 형태로 코딩했는데, 이렇게 하니까 제 눈에는 너무 지저분해 보이더라구요.

그래서 든 생각이 메뉴의 계층 구조나 헤더 같은 건 메타데이터로 간주하고, attribute를 이용해 기록한 후 프로그램을 구동할 때 읽어오자였습니다.

이를 바탕으로 제가 시도한 구현 방식은 다음과 같습니다.

  1. View의 custom menu control에서 표현될 Header, Parent, Icon을 각각 attribute 클래스로 분리하고 ViewModel 클래스에 속성으로 추가. 각 속성을 한 곳에 담아 제공하는 클래스(이하 속성 집합 클래스) 정의

  2. 1에서 분리한 속성만을 담는 MenuItem 클래스 구현

  3. 앱 시작 시 viewmodel을 추상화한 IViewModel 인터페이스를 상속하는 모든 클래스를 가져오고, 1에서 추가한 속성 집합 클래스과 함께 ioc container에 등록

  4. viewmodel cache 역할을 하는 클래스를 ioc container에 추가한 후 DI를 이용해 3을 주입

  5. cache 클래스에서 속성을 이용해 계층 구조의 MenuItem 클래스를 생성하고, ViewModel과 결합해 MainViewModel에 제공

  6. MenuItem에서 메뉴 선택 명령이 발생하면 해당하는 ViewModel로 화면 전환


성능이나 메모리 관리를 생각하면 제가 모르는 부가적인 구현이 남아있을 테지만 우선은 생각한 이론대로의 구현에는 성공했습니다.

그런데 단점이, 3의 구현이 상대적으로 지저분해집니다. 특히 1에서 추가한 속성별로 attribute를 따로 읽어오다 보니 이 부분만 해도 코드를 몇 줄 더 잡아먹네요. 나중에 속성이 더 늘어난다면 마찬가지로 함께 늘어날 테고요. 이건 리플렉션을 이용하면 어느 정도 해결되는 문제긴 하지만…

지금 수준만 해도 어느 정도 만족스럽긴 한데, 그래도 조금 더 깔끔하게 할 수 있지 않을까 하는 아쉬움도 들어서 조언을 구하고자 합니다.

  1. 특정한 속성의 집합을 갖는 클래스에 대해 해당 속성을 자동으로 클래스로 파싱해주는 기능이 있나요?

  2. 혹은 파싱하지 않고도 생성자에 속성 집합 클래스를 DI로 주입할 수 있을까요?

외부 라이브러리는 Microsoft Community Toolkit과 Microsoft DI 쓰고 있습니다. 가급적이면 BCL과 이 라이브러리 안에서 끝내고 싶은데, 비슷한 기능을 제공하는 다른 라이브러리가 있다면 이를 추천해주셔도 감사하겠습니다.

또 본문의 질문과는 별개로 보통 어떤 방식으로 메뉴를 구현하시는지 궁금하네요.

좋아요 3

계층 구조 형식인 메뉴 정보를 xml형식으로 로컬이던 DB던 어딘가에 들고 있고

<Menu>
    <AMenu PageType="" PageName="">
            <ASubMenu PageType="" PageName=""/>
    </AMenu>

    <BMenu PageType="" PageName="" />

    <CMenu>
            <CSubMenu PageType="" PageName="">
                <CSub_SubMenu PageType="" PageName=""/>
            <CSubMenu>
    </CMenu>
</Menu>

위 메뉴 정보 베이스 기본 구조의 POCO 클래스를 미리 정의해 둔 다음 파싱해서

TreeView에 바인딩하고 HierarchicalDataTemplate 을 사용해서 계층적인 UI 메뉴를 표현할 수 있을 듯 합니다.

좋아요 3

계층 구조의 메뉴는 ItemsSource와 템플릿으로 구성하면 되지 않을까요? 대략 아래의 방법으로 말이죠.

...
     <Menu DockPanel.Dock="Top" ItemsSource="{Binding MenuItems}">
         <Menu.ItemContainerStyle>
             <Style TargetType="{x:Type MenuItem}">
                 <Setter Property="Command" Value="{Binding Command}" />
             </Style>
         </Menu.ItemContainerStyle>
         <Menu.ItemTemplate>
             <HierarchicalDataTemplate DataType="{x:Type local:MenuItemInfo}" ItemsSource="{Binding Path=MenuItems}">
                 <TextBlock Text="{Binding Header}"/>
             </HierarchicalDataTemplate>
         </Menu.ItemTemplate>
     </Menu>
...

이 내용은 아마도 사용하는 용어가 달라 제가 이해가 안되는 것 같습니다.

메뉴에 특화된 바인딩된 모델만 있으면 될 것 같은데… 아닌가요?

좋아요 5

@aroooong
xml 형식으로 담아놨다가 파싱해서 HierarchicalDataTemplate로 쓰는 방법도 있겠군요! 멋진 아이디어 감사합니다. 참고하도록 하겠습니다.

@dimohy
제가 의도했던 것은 HeaderAttribute 같은 식으로 메뉴 표현에 사용되는 속성을 attribute로 정의해놓고 뷰모델 클래스에 붙이면, app.xaml.cs에서 ServiceProvider에 추가하면서 reflection으로 attribute를 가져와 클래스화한 후 Cache에 등록해 메뉴 구성에 이용할 수 있지 않을까 하는 생각이었습니다. 일단 아래와 같은 방식으로 구현에는 성공했습니다.

HeaderAttribute 클래스

[AttributeUsage(AttributeTargets.Class)]
public sealed class HeaderAttribute : Attribute
{
	public string? Header { get; set; }

	public HeaderAttribute(string? header) => Header = header;
}

메뉴에 등록할 ViewModel 클래스

[Header("Home")]
[IconType(IconType.Desktop)]
public partial class HomeViewModel : ViewModelBase

MenuItemMetaData 클래스

public sealed class MenuItemMetaData
{
	public string? Header { get; set; }
	public Type? Parent { get; set; }
	// Attribute가 추가될 때마다 MetaData 클래스에도 프로퍼티로 추가
}

App.xaml.cs

private IServiceProvider ConfigureServices()
{
	var service = new ServiceCollection();

	var types = AppDomain.CurrentDomain.GetAssemblies()
						 .SelectMany(c => c.GetTypes())
						 .Where(x => typeof(IViewModel).IsAssignableFrom(x) && !x.IsAbstract)
						 .ToList();

	Dictionary<Type, MenuItemMetaData> dictionaries = new();
	foreach (var type in types)
	{
		service.AddTransient(type);
		var header = type.GetCustomAttributes(typeof(HeaderAttribute), false).SingleOrDefault() as HeaderAttribute;
		var parent = type.GetCustomAttributes(typeof(ParentAttribute), false).SingleOrDefault() as ParentAttribute;

		if(header is null) continue;

		var metaData = new MenuItemMetaData 
		{
			Header = header.Header,
			Parent = parent?.Parent,
		};
		dictionaries.Add(type, metaData);
	}
	service.AddSingleton(provider => new ViewModelCache(provider, dictionaries));
	service.AddTransient<MainViewModel>();

	return service.BuildServiceProvider();
}

ViewModelCache 클래스

public ViewModelCache(IServiceProvider serviceProvider, Dictionary<Type, ToolItemMetadata> dictionaries)
{
	this._MenuItemHierarchy = BuildMenuItemHierarchy(serviceProvider, dictionaries);
}

앱을 시작할 때 이런 식으로 attribute가 붙은 viewmodel 클래스를 ServiceProvider에 추가한 후 Type을 메타데이터 클래스와 함께 cache에 넣어서 메뉴 계층을 구성하는데 이용하면 메뉴 구성을 좀더 유동적으로 할 수 있지 않을까 하는 생각이었습니다.

그런데… 정확히 제가 원하던 게 이미 나와있긴 하더라구요.

MEF는 말로만 들어보고 써보긴 커녕 공부 용도로 찾아본 기존 프로젝트들 중에서도 MEF를 사용하는 경우를 거의 못본 것 같은데 너무나 정확하게 제가 생각하던 기능이라 써볼까 싶네요…

좋아요 3

Property가 아니라 Attribute 였군요! T_T

생각하시는 방식 흥미롭습니다.

저같은 경우 ViewModel에 따라 메뉴를 추가되거나 위치가 변경되는 경우가 자주 있지 않았기 때문에 비슷한 고민은 하지 않았던 것 같네요.

좋아요 2

@dimohy
저도 생성자에서 ServiceProvider를 이용해 뷰모델의 타입을 명시해 가져온 후 계층을 구성하는 방식을 이용했는데 개인적으로 좀 지저분해지는 느낌이 있어서… 다른 방식이 없을까 싶었습니다.

좋아요 2