wpf에서 mvvm 패턴을 이용해 토이 프로젝트 느낌으로 프로그램을 개발하고 있는데, 계층 구조의 메뉴를 하드코딩하지 않고 다른 방법으로 해결하면 좋겠다는 생각이 들었습니다. 기존에는 생성자에 하위 메뉴나 상위 메뉴를 넣는 형태로 코딩했는데, 이렇게 하니까 제 눈에는 너무 지저분해 보이더라구요.
그래서 든 생각이 메뉴의 계층 구조나 헤더 같은 건 메타데이터로 간주하고, attribute를 이용해 기록한 후 프로그램을 구동할 때 읽어오자였습니다.
이를 바탕으로 제가 시도한 구현 방식은 다음과 같습니다.
View의 custom menu control에서 표현될 Header, Parent, Icon을 각각 attribute 클래스로 분리하고 ViewModel 클래스에 속성으로 추가. 각 속성을 한 곳에 담아 제공하는 클래스(이하 속성 집합 클래스) 정의
1에서 분리한 속성만을 담는 MenuItem 클래스 구현
앱 시작 시 viewmodel을 추상화한 IViewModel 인터페이스를 상속하는 모든 클래스를 가져오고, 1에서 추가한 속성 집합 클래스과 함께 ioc container에 등록
viewmodel cache 역할을 하는 클래스를 ioc container에 추가한 후 DI를 이용해 3을 주입
cache 클래스에서 속성을 이용해 계층 구조의 MenuItem 클래스를 생성하고, ViewModel과 결합해 MainViewModel에 제공
MenuItem에서 메뉴 선택 명령이 발생하면 해당하는 ViewModel로 화면 전환
성능이나 메모리 관리를 생각하면 제가 모르는 부가적인 구현이 남아있을 테지만 우선은 생각한 이론대로의 구현에는 성공했습니다.
그런데 단점이, 3의 구현이 상대적으로 지저분해집니다. 특히 1에서 추가한 속성별로 attribute를 따로 읽어오다 보니 이 부분만 해도 코드를 몇 줄 더 잡아먹네요. 나중에 속성이 더 늘어난다면 마찬가지로 함께 늘어날 테고요. 이건 리플렉션을 이용하면 어느 정도 해결되는 문제긴 하지만…
지금 수준만 해도 어느 정도 만족스럽긴 한데, 그래도 조금 더 깔끔하게 할 수 있지 않을까 하는 아쉬움도 들어서 조언을 구하고자 합니다.
특정한 속성의 집합을 갖는 클래스에 대해 해당 속성을 자동으로 클래스로 파싱해주는 기능이 있나요?
혹은 파싱하지 않고도 생성자에 속성 집합 클래스를 DI로 주입할 수 있을까요?
외부 라이브러리는 Microsoft Community Toolkit과 Microsoft DI 쓰고 있습니다. 가급적이면 BCL과 이 라이브러리 안에서 끝내고 싶은데, 비슷한 기능을 제공하는 다른 라이브러리가 있다면 이를 추천해주셔도 감사하겠습니다.
@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();
}
앱을 시작할 때 이런 식으로 attribute가 붙은 viewmodel 클래스를 ServiceProvider에 추가한 후 Type을 메타데이터 클래스와 함께 cache에 넣어서 메뉴 계층을 구성하는데 이용하면 메뉴 구성을 좀더 유동적으로 할 수 있지 않을까 하는 생각이었습니다.
그런데… 정확히 제가 원하던 게 이미 나와있긴 하더라구요.
MEF는 말로만 들어보고 써보긴 커녕 공부 용도로 찾아본 기존 프로젝트들 중에서도 MEF를 사용하는 경우를 거의 못본 것 같은데 너무나 정확하게 제가 생각하던 기능이라 써볼까 싶네요…