내용 보기

작성자

관리자 (IP : 172.17.0.1)

날짜

2020-07-10 03:43

제목

[WPF] ICommand의 메모리 누수 방지


ICommand의 메모리 누수 방지

ICommand의 CanExecute Command같은 경우는 마우스 및 키보드에 Input이 감지되면 매번 Command가 발생된다.
항상 ExecuteCommand가 실행 가능한지 체크 되어야 하기 때문에 기본적으로 마우스 및 키보드의 Input이 감지 될때마다 발생될 수 밖에 없다.
그런데, 사용되고 있는 View가 소멸 된 후에도 역시 매번 발생되는 현상을 볼 수 있다.


즉 View가 처음 생성되고 해당 View에 ViewModel이 서로 바인딩 처리 되어 있는 상태에서 UI들에 Command 처리가 되어 있는 경우 View가 Close되어 화면이 닫혀도 CanExecute Command는 매번 발생 되는 현상을 볼 수 있다.
위 처럼 쓸대없이 매번 CanExecute Command가 발생되는 현상을 막을려면 다음과 같이 WeakReference 를 사용하여 Command의 이벤트 처리를 하면 된다.

DelegateCommand.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Input;
 
namespace IntegratedManagementConsole.Commons
{
    public class DelegateCommand<T> : ICommand
    {
        #region Member Fields
        /// <summary>
        /// 해당 View가 제거 된 후 ViewModel이 더 이상 사용 되지 않은 경우에도 CanExecute Command가 항상 발생 되는 문제가 있어 <para />
        /// CanExecute EventHandler를 별도 List로 관리하여 처리 한다.
        /// </summary>
        private List<WeakReference> _canExecuteChangedHandlers;
        private bool _isAutomaticRequeryDisabled = false;
        private readonly Predicate<T> _canExecute;
        private readonly Action<T> _execute;
        #endregion
 
        #region Construct
        public DelegateCommand(Action<T> execute)
            : this(execute, null)
        {
        }
 
        public DelegateCommand(Action<T> execute, Predicate<T> canExecute)
            : this(execute, canExecute, false)
        {
        }
 
        public DelegateCommand(Action<T> execute, Predicate<T> canExecute, bool isAutomaticRequeryDisabled)
        {
            if (execute == null)
            {
                throw new ArgumentNullException("executeMethod");
            }
 
            _execute = execute;
            _canExecute = canExecute;
            _isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
        }
        #endregion
 
        /// <summary>
        /// Property to enable or disable CommandManager's automatic requery on this command
        /// </summary>
        public bool IsAutomaticRequeryDisabled
        {
            get
            {
                return _isAutomaticRequeryDisabled;
            }
            set
            {
                if (_isAutomaticRequeryDisabled != value)
                {
                    if (value)
                    {
                        CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
                    }
                    else
                    {
                        CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
                    }
 
                    _isAutomaticRequeryDisabled = value;
                }
            }
        }
 
        #region ICommand 구현
        public event EventHandler CanExecuteChanged
        {
            add
            {
                if (!_isAutomaticRequeryDisabled)
                {
                    CommandManager.RequerySuggested += value;
                }
                CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, -1);
            }
            remove
            {
                if (!_isAutomaticRequeryDisabled)
                {
                    CommandManager.RequerySuggested -= value;
                }
 
                CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
            }
        }
 
        public bool CanExecute(object parameter)
        {
            if (_canExecute == null)
                return true;
 
            return _canExecute((parameter == null) ?
                default(T) : (T)Convert.ChangeType(parameter, typeof(T)));
        }
 
        public void Execute(object parameter)
        {
            _execute((parameter == null) ? default(T) : (T)Convert.ChangeType(parameter, typeof(T)));
        }
        #endregion
 
        /// <summary>
        /// Raises the CanExecuteChaged event
        /// </summary>
        public void RaiseCanExecuteChanged()
        {
            OnCanExecuteChanged();
        }
 
        protected virtual void OnCanExecuteChanged()
        {
            CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
        }
    }
 
    /// <summary>
    /// View가 소멸되고 해당 ViewModel이 사용되지 않는 Command가 메모리에 계속 상주해 있는 문제를 해결하는 클래스
    /// Command EventHandler를 약한 참조를 사용하여 연결한다.
    /// https://github.com/crosbymichael/mvvm-async/blob/master/MVVM-Async/Commands/DelegateCommand.cs#L44
    /// </summary>
    internal class CommandManagerHelper
    {
        internal static void CallWeakReferenceHandlers(List<WeakReference> handlers)
        {
            if (handlers != null)
            {
                // Take a snapshot of the handlers before we call out to them since the handlers
                // could cause the array to me modified while we are reading it.
                EventHandler[] callees = new EventHandler[handlers.Count];
                int count = 0;
                for (int i = handlers.Count - 1; i >= 0; i--)
                {
                    WeakReference reference = handlers[i];
                    EventHandler handler = reference.Target as EventHandler;
                    if (handler == null)
                    {
                        // Clean up old handlers that have been collected
                        handlers.RemoveAt(i);
                    }
                    else
                    {
                        callees[count] = handler;
                        count++;
                    }
                }
 
                // Call the handlers that we snapshotted
                for (int i = 0; i < count; i++)
                {
                    EventHandler handler = callees[i];
                    handler(null, EventArgs.Empty);
                }
            }
        }
 
        internal static void AddHandlersToRequerySuggested(List<WeakReference> handlers)
        {
            if (handlers != null)
            {
                foreach (WeakReference handlerRef in handlers)
                {
                    EventHandler handler = handlerRef.Target as EventHandler;
                    if (handler != null)
                    {
                        CommandManager.RequerySuggested += handler;
                    }
                }
            }
        }
 
        internal static void RemoveHandlersFromRequerySuggested(List<WeakReference> handlers)
        {
            if (handlers != null)
            {
                foreach (WeakReference handlerRef in handlers)
                {
                    EventHandler handler = handlerRef.Target as EventHandler;
 
                    if (handler != null)
                    {
                        CommandManager.RequerySuggested -= handler;
                    }
                }
            }
        }
 
        internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler)
        {
            AddWeakReferenceHandler(ref handlers, handler, -1);
        }
 
        internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler, int defaultListSize)
        {
            if (handlers == null)
            {
                handlers = (defaultListSize > 0 ? new List<WeakReference>(defaultListSize) : new List<WeakReference>());
            }
 
            handlers.Add(new WeakReference(handler));
        }
 
        internal static void RemoveWeakReferenceHandler(List<WeakReference> handlers, EventHandler handler)
        {
            if (handlers != null)
            {
                for (int i = handlers.Count - 1; i >= 0; i--)
                {
                    WeakReference reference = handlers[i];
                    EventHandler existingHandler = reference.Target as EventHandler;
 
                    if ((existingHandler == null|| (existingHandler == handler))
                    {
                        // Clean up old handlers that have been collected
                        // in addition to the handler that is to be removed.
                        handlers.RemoveAt(i);
                    }
                }
            }
        }
    }
}
cs

위 코드의 DelegateCommand<T>를 사용하면 매번 CanExecute Command가 발생되진 않는다.

DelegateCommand<T> 사용 예

#region Commands
private DelegateCommand<object> _testSendMailCommand;
public ICommand TestSendMailCommand
{
    get
    {
        return _testSendMailCommand ??
                (_testSendMailCommand = new DelegateCommand<object>(
                    param => this.ExecuteTestSendMailCommand(),
                    param => this.CanExecuteTestSendMailCommand(), true));
    }
}
#endregion  // Commands
 
public string TestSendMailTo
{
 
    get
    {
        return _testSendMailTo;
    }
    set
    {
        _testSendMailTo = value;
        this.OnPropertyChanged("TestSendMailTo");
 
        if (_testSendMailCommand != null)
            _testSendMailCommand.RaiseCanExecuteChanged();
    }
}
 
private bool CanExecuteTestSendMailCommand()
{
    return (string.IsNullOrWhiteSpace(TestSendMailTo) == false);
}
cs

위 처럼 Command 바인딩 처리를 하게 되면 CanExecute Command는 해당 Command가 발생되기 전 한번 호출이 되고 그 이 후는 더이상 매번 발생되지 않는다.

때문에 CanExecuteCommand의 로직에 맞게 특정 속성 값이 변경 될때마다 RaiseCanExecuteChanged() 메서드를 수동으로 직접 호출해 주어야 한다.

출처1

출처2