У меня есть прикрепленное свойство для использования в сетке данных, чтобы можно было использовать SelectedItems в моей модели представления. Код такой:

public class DataGridSelectedItemsAttachedProperty
    {
        #region SelectedItems
        ///
        /// SelectedItems Attached Dependency Property
        ///
        public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.RegisterAttached("SelectedItems", typeof(IList),
        typeof(DataGridSelectedItemsAttachedProperty),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
        new PropertyChangedCallback(OnSelectedItemsChanged)));

        public static IList GetSelectedItems(DependencyObject d)
        {
            return (IList)d.GetValue(SelectedItemsProperty);
        }

        public static void SetSelectedItems(DependencyObject d, IList value)
        {
            d.SetValue(SelectedItemsProperty, value);
        }

        private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            DataGrid miDg = (DataGrid)d;
            miDg.SelectionChanged += dataGrid_SelectionChanged;
            miDg.Unloaded += dataGrid_Unloaded;
        }

        private static void dataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            DataGrid miDg = (DataGrid)sender;
            //Get list box's selected items.
            IEnumerable miDgSelectedItems = miDg.SelectedItems;
            //Get list from model
            IList ModelSelectedItems = GetSelectedItems(miDg);

            //Update the model
            ModelSelectedItems.Clear();

            if (miDg.SelectedItems != null)
            {
                foreach (var item in miDg.SelectedItems)
                    ModelSelectedItems.Add(item);
            }
            SetSelectedItems(miDg, ModelSelectedItems);
        }


        private static void dataGrid_Unloaded(object sender, RoutedEventArgs e)
        {
            DataGrid miDg = sender as DataGrid;
            miDg.SelectionChanged -= dataGrid_SelectionChanged;
            miDg.Unloaded -= dataGrid_Unloaded;
        }
        #endregion
    }

Проблема в том, что эта сетка данных находится в элементе управления вкладками, запускается выгрузка события, поэтому событие отменяется, а затем SelectedItems больше не уведомляется в модель представления.

Итак, я хотел бы знать, как решить эту проблему, возможно, отменить подписку на события в другом месте вместо события выгрузки?

Спасибо.

0
Álvaro García 30 Дек 2017 в 19:34

2 ответа

Лучший ответ

когда я закрываю пользовательский элемент управления, присоединенное свойство будет восстановлено, потому что ни один объект не ссылается на него.

Это неправда. Если вы удалите код, отменяющий регистрацию событий, любые элементы управления, использующие присоединенное свойство, будут жить вечно. Зачем? Потому что регистрируемые вами обработчики событий статичны. Это означает, что элемент управления будет содержать ссылку на что-то статическое, не позволяющее сборщику мусора когда-либо его собрать.

Первое возможное решение этой проблемы - использовать шаблон слабых событий при регистрации событий. По указанной выше причине я всегда использую шаблон слабого события при регистрации событий для моих собственных прикрепленных свойств.

Раздражает в этом решении то, что оно требует довольно большого количества шаблонного кода. Вы должны создать новую реализацию WeakEventManager для каждого нового типа события. Затем, чтобы получать слабые события, вам необходимо реализовать интерфейс (EDIT: если вы не используете .NET 4.5 или выше), а это означает, что у вас не может быть статического обработчика. Итак, вам нужен класс, реализующий интерфейс IWeakEventListner, а также создание и управление экземплярами этого класса в ваших связанных событиях свойств.

Поэтому решение, которое я бы порекомендовал вам, состоит в том, чтобы фактически создать подкласс класса DataGrid и добавить эту функциональность как обычное свойство зависимости. Если вы сделаете это таким образом, вам вообще не придется регистрировать события (есть защищенные методы, которые вы можете переопределить), и не стоит беспокоиться о потенциальных утечках памяти. Причина, по которой я рекомендовал бы это решение, состоит в том, что, по моему опыту, мне пришлось переопределить класс DataGrid по множеству других причин, многие из них могут быть достигнуты с присоединенными свойствами, но некоторые из них не могут.

Настоящая проблема заключается в том, что реализация WPF DataGrid довольно недоработана (мое личное мнение). Есть ошибки, поведение по умолчанию, которое мне не нравится, а также неполные или нереализованные функции (например, поддержка копирования, но не вставки; или конкретная проблема, которую, я думаю, вы пытаетесь решить: привязываемые элементы SelectedItems). Легче всего исправить все эти проблемы, просто создав подкласс DataGrid.

1
Dave M 31 Дек 2017 в 18:06

Я столкнулся с тем же вопросом, но пришел к выводу, что в этом случае нет необходимости отказываться от подписки на события (спасибо за комментарии от Альваро Гарсии и Blechdose, которые указывают мне в этом направлении).

На самом деле утечки памяти из-за обработчика событий - это проблема с одной стороны. Причина этой проблемы описана здесь: https://stackoverflow.com/a/4526840/12797700. Используя этот код miDg.SelectionChanged += dataGrid_SelectionChanged;, вы добавляете ссылку на объект, который хранит метод dataGrid_SelectionChanged в объекте miDg. Из-за этого сборщик мусора не может удалить объект, в котором хранится метод dataGrid_SelectionChanged, пока объект miDg жив.

Однако статический объект ничего не знает об объекте miDg, и GC может удалить объект miDg, даже если событие обработано.

Вы можете скачать тестовый проект, демонстрирующий это поведение, по следующей ссылке. Он также демонстрирует, как воспроизвести проблему утечки памяти путем обработки события.

https://github.com/Drreamer/AttachedPropertyMemoryTest

2
Drreamer 27 Апр 2020 в 13:33