본문 바로가기

SW개발/c#

[WPF] Messenger 구현 (MVVM)

처음 WPF 프로젝트를 다룰 때 MVVM 적용하는데 필요한 도구 중에 MVVMLight을 매우 유용하게 사용했습니다.

그 중에 Messenger 라는 클래스가 있는데 특정 Type을 기억해두고 해당 클래스를 Send와 Register를 통해 ViewModel간의 data 전달하는데 매우 유용하게 썼었던지라 해당 Messenger를 다른프로젝트에서 따로 아래와 같이 구현해보았습니다. 

 

정확하게 동일하게 작동하는지는 알 수 없지만 직접 고민해서 작성했던 코드이고 현재까지 사용하는데 있어서 의도대로 작동하고있고 별다른 이슈도 없어서 나름 기록하고자 포스트합니다.

 

public class Messenger
{
	private readonly Dictionary<Type, Action<object>> _actions;
	private readonly List<Tuple<Delegate, Action<object>>> _actionTuples;
	private static Messenger _instance;
	private static readonly object _lock = new object();

	//Singletone Instance
	public static Messenger Instance { get { lock (_lock) { return _instance ??= new Messenger(); } } }

	//Singletone 외부에서 생성이 안되도록
	private Messenger()
	{
		//실제로 Subscribe된 Delegate를 실행하는 곳.
		_actions = new Dictionary<Type, Action<object>>();
		//Subscribe한 Delegate를 Action<object> 형식으로 만들어서 가지고 있음.
		_actionTuples = new List<Tuple<Delegate, Action<object>>>();
	}

	public void Subscribe<T>(Action<T> action)
	{
		Action<object> input = null;
		input = _actionTuples.Find(x => x.Item1.Equals(action))?.Item2;
		if (input == null)
		{
        	//Action<T>를 Action<object> 형태로 변형해서 List에 추가.
			input = new Action<object>(o => action((T)o));
			_actionTuples.Add(new Tuple<Delegate, Action<object>>(action, input));
		}
		else
		{
			return;
		}
		//Action<T>가 아닌 Action<object>를 Dctionary에 넣어준다.
		if (!_actions.ContainsKey(typeof(T)))
		{
			_actions.Add(typeof(T), input);
		}
		else
		{
			_actions[typeof(T)] += input;
		}
	}

	public void Unsubscribe<T>(Action<T> action)
	{
		//Action<T>로 Action<object>를 찾고 Dictionary에서 제거
		Action<object> input = null;
		input = _actionTuples.Find(x => x.Item1.Equals(action))?.Item2;
		if (input == null)
		{
			return;
		}
		else
		{
			_actions[typeof(T)] -= input;
		}
	}

	public void Publish<T>(T obj)
	{
		if (!_actions.ContainsKey(typeof(T)))
		{
			return;
		}        
		_actions[typeof(T)]?.Invoke(obj);
	}
}

Messenger 클래스 자체는 싱글톤으로 만들었는데 다른 Thread에서 접근할 소지가 있어서 따로 lock 키워드를 통해 동기화를 하려고 하였습니다.

 

Type에 따른 delegate를 저장할때 Action<T>를 지정하고 싶으나 Dictionary에 제네릭 형태로는 저장할 수가 없어서 모든 인수를 받을수 있는 object로 지정하였습니다. 때문에 생기는 문제가 있는데 2가지가 있는데

 

1. Dictionary에 Action<T> 형태로 추가가 안됩니다.

Subscribe를 할 때는 Action<T>로 받았으니 그대로는 Dictionary에 Add할 수가 없다는 것입니다. 그래서 Action<T>를 Action<object>로 한번 형태를 변형해서 Dictionary에 추가해주게 되었습니다.

 

2. Subscribe 했던 객체가 유지가 안됩니다.

Subscribe 했던 Action<T>를 Action<object>로 변형해서 Dictionary에 추가를 해주었으니 원래 추가하려고 했던 Action<T>는 쓰이지 않게 됩니다. 여기서 생기는 문제는 Unsubcribe를 할 수가 없다는 것입니다. ViewModel에서 같은 함수를 Subscribe하고 다시 Unsubscribe 하는데 애초에 의도했던 함수는 안쓰고 새로 생성된 Action<object> 객체를 사용하고 있었으니 해제를 하려고해도 찾을수가 없는 것이죠 그래서 해당 문제를 해결하기 위해 List로 따로 관리해주고 있습니다.

 

 

 

현재는 해당 코드를 통해 의도한대로 잘 사용은 하고 있으나 더 좋은 코드가 또 있을지도 모른다는 생각은 드는데 더이상 감당은 안되네요. 좀 더 고민은 해봐야할 것 같습니다.