Как лучше всего распространять уведомления об изменениях вверх по иерархической структуре для привязки? - PullRequest
2 голосов
/ 10 января 2011

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

Контекст, в котором я нуждаюсь, это маленький RSS-FeedReader, который я пытаюсь создать. Это самые важные объекты и аспекты моей модели:

Составной интерфейс:

public interface IFeedComposite : INotifyPropertyChanged
{
    string Title { get; set; }

    int UnreadFeedItemsCount { get; }

    ObservableCollection<FeedItem> FeedItems { get; }
}

FeedComposite (он же папка)

public class FeedComposite : BindableObject, IFeedComposite
    {
        private string title = "";
        public string Title
        {
            get { return title; }
            set
            {
                title = value;
                NotifyPropertyChanged("Title");
            }
        }

        private ObservableCollection<IFeedComposite> children = new ObservableCollection<IFeedComposite>();
        public ObservableCollection<IFeedComposite> Children
        {
            get { return children; }
            set
            {
                children.Clear();
                foreach (IFeedComposite item in value)
                {
                    children.Add(item);
                }
                NotifyPropertyChanged("Children");
            }
        }

        public FeedComposite() { }

        public FeedComposite(string title)
        {
            Title = title;
        }

        public ObservableCollection<FeedItem> FeedItems
        {
            get
            {
                ObservableCollection<FeedItem> feedItems = new ObservableCollection<FeedItem>();
                foreach (IFeedComposite child in Children)
                {
                    foreach (FeedItem item in child.FeedItems)
                    {
                        feedItems.Add(item);
                    }
                }
                return feedItems;
            }
        }


    public int UnreadFeedItemsCount
    {
        get
        {
            return (from i in FeedItems
                    where i.IsUnread
                    select i).Count();
        }
    }

Feed:

public class Feed : BindableObject, IFeedComposite
    {
        private string url = "";
        public string Url
        {
            get { return url; }
            set
            {
                url = value;
                NotifyPropertyChanged("Url");
            }
        }

        ...


        private ObservableCollection<FeedItem> feedItems = new ObservableCollection<FeedItem>();
        public ObservableCollection<FeedItem> FeedItems
        {
            get { return feedItems; }
            set
            {
                feedItems.Clear();
                foreach (FeedItem item in value)
                {
                    AddFeedItem(item);
                }
                NotifyPropertyChanged("Items");
            }
        }

        public int UnreadFeedItemsCount
        {
            get
            {
                return (from i in FeedItems
                        where i.IsUnread
                        select i).Count();
            }
        }

        public Feed() { }

        public Feed(string url)
        {
            Url = url;
        }

Хорошо, вот в чем дело, если я привяжу TextBlock.Text к UnreadFeedItemsCount, не будет простых уведомлений, когда элемент помечен как непрочитанный, поэтому один из моих подходов заключался в обработке PropertyChanged событие каждого FeedItem и если IsUnread -Property изменяется, у меня есть Feed сделать уведомление, что свойство UnreadFeedItemsCount было изменено. При таком подходе мне также нужно обрабатывать все PropertyChanged события всех Feeds и FeedComposites в Children из FeedComposite, исходя из его звучания, должно быть очевидно, что это не очень хорошая идея Вы должны быть очень осторожны, чтобы элементы никогда не добавлялись и не удалялись из какой-либо коллекции, не подключив сначала обработчик событий PropertyChanged.

Также: что мне делать с CollectionChanged-событиями, которые также обязательно приводят к изменению суммы количества непрочитанных предметов? Похоже, больше удовольствия от обработки событий.

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

1 Ответ

1 голос
/ 10 января 2011

Ну, я подумал, что хотел бы ответить на ваш вопрос, чтобы посмотреть, что я приду. Это не проверено, и это в некотором роде то же самое, что у вас уже было. Основное отличие, которое я сделал, - добавленные методы для обработки добавления и удаления каналов, которые обрабатывают привязку события, необходимую для его работы. Theres немного кода, так что здесь идет,

Я весь мой код в одном файле, вам нужно немного изменить, если вы хотите в отдельных файлах.

Сначала метод расширения Groovy для PropertyChangedEventHandler Вам не нужно его использовать, но мне оно очень нравится.

public static class NotifyPropertyChangedExtention
    {
        public static void Raise<T, TP>(this PropertyChangedEventHandler pc, T source, Expression<Func<T, TP>> pe)
        {
            if (pc != null)
            {
                pc.Invoke(source, new PropertyChangedEventArgs(((MemberExpression)pe.Body).Member.Name));
            }
        }
    }

Во-вторых, FeedItem за вычетом фида :) У меня есть проверка, чтобы вызвать событие изменения только тогда, когда значение действительно изменится. Здесь вы можете увидеть используемый метод расширения Raise, без всяких строк.

class FeedItem : INotifyPropertyChanged
{
    private bool _isUnread;
    public event PropertyChangedEventHandler PropertyChanged;

    public bool IsUnread
    {
        get { return _isUnread; }
        set
        {
            if (_isUnread != value)
            {
                _isUnread = value;
                PropertyChanged.Raise(this, x => x.IsUnread);
            }
        }
    }
}

Теперь интерфейсы, которые я сделал, немного отличаются, так как мои папки могут содержать как другие папки, так и каналы.

internal interface IFeedComposite : INotifyPropertyChanged
{
    string Title { get; set; }
    int UnreadFeedItemsCount { get; }
}

internal interface IFeedFolder : IFeedComposite
{
    ObservableCollection<IFeedFolder> FeedFolders { get; }
    ObservableCollection<IFeed> Feeds { get; }
    void AddFeed(IFeed newFeed);
    void RemoveFeed(IFeed feedToRemove);
    void AddFeedFolder(IFeedFolder newFeedFolder);
    void RemoveFeedFolder(IFeedFolder feedFolderToRemove);
}

internal interface IFeed : IFeedComposite
{
    ObservableCollection<FeedItem> FeedItems { get; }
    void AddFeedItem(FeedItem newFeedItem);
    void RemoveFeedItem(FeedItem feedItemToRemove);
}

Теперь в классе каналов метод AddFeedItem перехватывает для вас событие изменения свойства и, если оно помечено как непрочитанное, вызывает событие изменения свойства для счетчика. Вы можете перегрузить этот метод, чтобы принять список элементов, а затем, после того как они были добавлены в список, если они не прочитаны, вызвать одно событие изменения свойства для всех них.

class Feed : IFeed
{
    private readonly ObservableCollection<FeedItem> _feedItems = new ObservableCollection<FeedItem>();

    public event PropertyChangedEventHandler PropertyChanged;
    public string Title { get; set; }

    public int UnreadFeedItemsCount
    {
        get
        {
            return (from i in FeedItems
                    where i.IsUnread
                    select i).Count();
        }
    }

    public ObservableCollection<FeedItem> FeedItems
    {
        get { return _feedItems; }
    }

    public void AddFeedItem(FeedItem newFeed)
    {
        newFeed.PropertyChanged += NewFeedPropertyChanged;
        _feedItems.Add(newFeed);
        PropertyChanged.Raise(this, x => x.FeedItems);
        if (newFeed.IsUnread)
        {
            PropertyChanged.Raise(this, x => x.UnreadFeedItemsCount);
        }
    }

    public void RemoveFeedItem(FeedItem feedToRemove)
    {
        _feedItems.Remove(feedToRemove);
        PropertyChanged.Raise(this, x => x.FeedItems);
        if (feedToRemove.IsUnread)
        {
            PropertyChanged.Raise(this, x => x.UnreadFeedItemsCount);
        }
    }

    void NewFeedPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "IsUnread")
        {
            PropertyChanged.Raise(this, x => x.UnreadFeedItemsCount);
        }
    }
}

Теперь класс FeedFolder, почти такой же, как класс каналов, но этот может содержать список каналов и список папок каналов (которые содержат свои собственные каналы). Вы можете легко добавить метод или свойство, чтобы при необходимости возвращать все фид-элементы из фидов и папок. Опять же, различные проверки, чтобы вызвать события изменения только при необходимости.

class FeedFolder : IFeedFolder
{
    private readonly ObservableCollection<IFeedFolder> _feedFolders = new ObservableCollection<IFeedFolder>();
    private readonly ObservableCollection<IFeed> _feeds = new ObservableCollection<IFeed>();

    public event PropertyChangedEventHandler PropertyChanged;
    public string Title { get; set; }

    public int UnreadFeedItemsCount
    {
        get { return Feeds.Sum(x => x.UnreadFeedItemsCount) + FeedFolders.Sum(x => x.UnreadFeedItemsCount); }
    }

    public ObservableCollection<IFeedFolder> FeedFolders
    {
        get { return _feedFolders; }
    }

    public ObservableCollection<IFeed> Feeds
    {
        get { return _feeds; }
    }

    public void AddFeed(IFeed newFeed)
    {
        newFeed.PropertyChanged += NewFeedPropertyChanged;
        _feeds.Add(newFeed);
        PropertyChanged.Raise(this, x => x.Feeds);
        if (newFeed.UnreadFeedItemsCount > 0)
        {
            PropertyChanged.Raise(this, x => x.UnreadFeedItemsCount);
        }
    }

    void NewFeedPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "UnreadFeedItemsCount")
        {
            PropertyChanged.Raise(this, x => x.UnreadFeedItemsCount);
        }
    }

    public void RemoveFeed(IFeed feedToRemove)
    {
        _feeds.Remove(feedToRemove);
        PropertyChanged.Raise(this, x => x.Feeds);
        if (feedToRemove.UnreadFeedItemsCount > 0)
        {
            PropertyChanged.Raise(this, x => x.UnreadFeedItemsCount);
        }
    }

    public void AddFeedFolder(IFeedFolder newFeedFolder)
    {
        newFeedFolder.PropertyChanged += NewFeedPropertyChanged;
        _feedFolders.Add(newFeedFolder);
        PropertyChanged.Raise(this, x => x.FeedFolders);
        if (newFeedFolder.UnreadFeedItemsCount > 0)
        {
            PropertyChanged.Raise(this, x => x.UnreadFeedItemsCount);
        }
    }

    public void RemoveFeedFolder(IFeedFolder feedFolderToRemove)
    {
        _feedFolders.Remove(feedFolderToRemove);
        PropertyChanged.Raise(this, x => x.FeedFolders);
        if (feedFolderToRemove.UnreadFeedItemsCount > 0)
        {
            PropertyChanged.Raise(this, x => x.UnreadFeedItemsCount);
        }
    }
}

Теперь для использования, помните, я не проверял это, но это должно быть в основном правильно.

var myFolder = new FeedFolder();
var myFeed = new Feed();
var myFeedItem = new FeedItem();
myFeedItem.IsUnread = true;
myFeed.AddFeedItem(myFeedItem);
myFolder.AddFeed(myFeed);

var mySecondFeedItem = new FeedItem();

//add a second feeditem to feed, but it is marked as read, so no notifications raised for unread count.
myFeed.AddFeedItem(mySecondFeedItem);

//this should fire off change events all the way up to the folder
mySecondFeedItem.IsUnread = true;
...