【问题标题】:Architecting a collection of related items of different types构建不同类型的相关项目的集合
【发布时间】:2018-10-17 23:11:27
【问题描述】:
public class Base : ICustomItem
{
}

public class Book : Base
{
    public List<Author> Authors;
    public List<Bookmark> Bookmarks;
}

public class Author : Base
{
}

public class Bookmark : Base
{
}

public ObservableCollection(ICustomItem) MyItems;

public ListCollectionView MyItemsView { get; } = new ListCollectionView(MyItems);

通过这样的设置,我可以显示一个包含书籍、作者和书签的单数列表。我可以深入研究一本书,并查看特定书籍的作者和书签。

问题 #1:我想深入了解一位作者,并查看所有作者的书籍。最重要的是,我想查看该作者所有书籍的所有书签。

一个简单的解决方案是将适当的列表添加到其他类中。例如。 public List&lt;Book&gt; BooksAuthor 类。但是,当您开始添加更多类别(例如,GenrePublisherLanguage 等)时,情况就会失控

问题 2:我还希望能够按任何选定标签的数量对我的列表进行排序,包括任何相关的标签类型:

MyItemsView.SortDescriptions.Add(new SortDescription("Bookmarks.Count", Descending);

作者拥有的书签数量应该是其所有书籍的所有书签的总和。

什么是构建此类数据的好方法,以便不必为每种类型维护多个查找集合?使用Book 作为事实来源不是一个好方法吗?理想情况下,我可以按任何标签类型进行排序。

在我的实际应用程序中,我已经解决了问题 1:当深入研究时。一个Author,我在MyItems 中找到所有Book 与匹配的作者,然后从拉出的书籍列表中拉出所有GenrePublisher 等。通过这种方式,我可以根据作者提供的书籍标签显示作者拥有的所有标签。 (我在列表视图模型的范围内执行此操作,因为我知道要深入研究哪个项目并可以访问主要的 MyItems 集合)

但是,使用这种方法我似乎无法解决问题 #2。为了能够对Bookmarks.Count 进行排序,我需要将其移动到Base 并以某种方式为每个相关的标签类型填充它。

如何在不让每个AuthorBookmark 访问全局项目集合(感觉像是一个很大的禁忌)或维护列表的情况下将此类信息公开给每个非Book 标记类型每种标签类型的所有相关标签(感觉非常低效)?

编辑 1: 能否定义“标签”和“标签类型”并再举几个例子?

我正在使用标签来定义我想放入列表中的任何类型的项目。 BookAuthorBookmark 都是标签。 LanguageGenre 也将是标签。一本书有作者和语言,就像语言有书籍和作者一样。

编辑2: 即使您不打算拥有一个后备数据库,您也应该从复习您的实体关系模型知识中受益

我知道这是一个高度相关的结构。我确实有一个后备数据库,但它在数据库中的存储方式与如何构造要绑定到 ListCollectionView 的数据的关系不大。

【问题讨论】:

  • 你能定义“标签”和“标签类型”并再举几个例子吗?
  • 即使您不打算拥有一个后备数据库,您也应该受益于复习您的Entity-Relationship Model 知识。
  • 您必须改进这个过于宽泛的问题才能获得您所寻求的关注。一小段不完整的代码和一堵墙的文字并不能澄清整个问题。
  • 此外,尽管您将问题标记为 mvvmdata-binding,但重要的是要注意,作为一个过程,您只是一步(或两步)之前 那个。
  • 我已经提供了足够的代码来尝试我的要求。创建书籍、作者和书签的统一列表,然后按总书签对列表进行排序。作者书签计数是在他们所写的书中制作的书签的总和。书籍书签计数是显而易见的。书签书签计数为 1。

标签: c# wpf mvvm data-binding


【解决方案1】:

我想我希望类型之间的关系尽可能空灵。虽然大多数类型很容易相关,但有些类型具有复合键或奇怪的关系,而你永远不知道......所以我将从类型本身中外部化相关类型的发现。我们中只有少数幸运儿拥有全球唯一的一​​致密钥类型。

我可以想象让所有类型都成为观察者和可观察者。我从来没有大声做过这样的事情......至少,不是这样,但这是一个有趣的可能性......并且给了 500 分,我认为值得一试;-)

我使用Tag 一词来关注您的评论。也许Base 对您更有意义?无论如何,在下文中,Tag 是一种通知观察标签并监听可观察标签的类型。我将observables 设为Tag.Subscription 的列表。通常,您只会有一个 IDisposable 实例的列表,因为这就是 observable 通常提供的所有内容。这样做的原因是Tag.Subscription 让您发现底层Tag...以便您可以在派生类型中为您的类型的列表属性刮取订阅(如下面的AuthorBook 所示。 )

我设置了Tag 订阅者/通知机制,无需 本身就可以工作......只是为了隔离机制。我假设大多数Tags 都会有值...但也许有例外。

public interface ITag : IObservable<ITag>, IObserver<ITag>, IDisposable
{
  Type TagType { get; }
  bool SubscribeToTag( ITag tag );
}

public class Tag : ITag
{
  protected readonly List<Subscription> observables = new List<Subscription>( );
  protected readonly List<IObserver<ITag>> observers = new List<IObserver<ITag>>( );
  bool disposedValue = false;

  protected Tag( ) { }

  IDisposable IObservable<ITag>.Subscribe( IObserver<ITag> observer )
  {
    if ( !observers.Contains( observer ) )
    {
      observers.Add( observer );
      observer.OnNext( this ); //--> or not...maybe you'd set some InitialSubscription state 
                               //--> to help the observer distinguish initial notification from changes
    }
    return new Subscription( this, observer, observers );
  }

  public bool SubscribeToTag( ITag tag )
  {
    if ( observables.Any( subscription => subscription.Tag == tag ) ) return false; //--> could throw here
    observables.Add( ( Subscription ) tag.Subscribe( this ) );
    return true;
  }

  protected void Notify( ) => observers.ForEach( observer => observer.OnNext( this ) );

  public virtual void OnNext( ITag value ) { }

  public virtual void OnError( Exception error ) { }

  public virtual void OnCompleted( ) { }

  public Type TagType => GetType( );

  protected virtual void Dispose( bool disposing )
  {
    if ( !disposedValue )
    {
      if ( disposing )
      {
        while ( observables.Count > 0 )
        {
          var sub = observables[ 0 ];
          observables.RemoveAt( 0 );
          ( ( IDisposable ) sub ).Dispose( );
        }
      }
      disposedValue = true;
    }
  }

  public void Dispose( )
  {
    Dispose( true );
  }

  protected sealed class Subscription : IDisposable
  {
    readonly WeakReference<Tag> tag;
    readonly List<IObserver<ITag>> observers;
    readonly IObserver<ITag> observer;

    internal Subscription( Tag tag, IObserver<ITag> observer, List<IObserver<ITag>> observers )
    {
      this.tag = new WeakReference<Tag>( tag );
      this.observers = observers;
      this.observer = observer;
    }

    void IDisposable.Dispose( )
    {
      if ( observers.Contains( observer ) ) observers.Remove( observer );
    }

    public Tag Tag
    {
      get
      {
        if ( tag.TryGetTarget( out Tag target ) )
        {
          return target;
        }
        return null;
      }
    }
  }
}

如果绝对所有标签都有值,您可以将以下实现与上述实现合并......但我认为将它们分开感觉更好。

public interface ITag<T> : ITag
{
  T OriginalValue { get; }
  T Value { get; set; }
  bool IsReadOnly { get; }
}

public class Tag<T> : Tag, ITag<T>
{
  T currentValue;

  public Tag( T value, bool isReadOnly = true ) : base( )
  {
    IsReadOnly = isReadOnly;
    OriginalValue = value;
    currentValue = value;
  }

  public bool IsReadOnly { get; }

  public T OriginalValue { get; }

  public T Value
  {
    get
    {
      return currentValue;
    }
    set
    {
      if ( IsReadOnly ) throw new InvalidOperationException( "You should have checked!" );
      if ( Value != null && !Value.Equals( value ) )
      {
        currentValue = value;
        Notify( );
      }
    }
  }
}

虽然这看起来有点忙,但主要是普通订阅机制和可处置性。派生类型将非常简单。

注意受保护的Notify() 方法。我开始将其放入界面中,但意识到从外部世界访问它可能不是一个好主意。

所以...举个例子;这是一个示例Author。注意AddBook 是如何建立相互关系的。并非每种类型都有这样的方法......但它说明了它是多么容易:

public class Author : Tag<string>
{
  public Author( string name ) : base( name ) { }

  public void AddBook( Book book )
  {
    SubscribeToTag( book );
    book.SubscribeToTag( this );
  }

  public IEnumerable<Book> Books
  {
    get
    {
      return
        observables
        .Where( o => o.Tag is Book )
        .Select( o => ( Book ) o.Tag );
    }
  }

  public override void OnNext( ITag value )
  {
    switch ( value.TagType.Name )
    {
      case nameof( Book ):
        Console.WriteLine( $"{( ( Book ) value ).CurrentValue} happened to {CurrentValue}" );
        break;
    }
  }
}

...和Book 会相似。关于相互关系的另一种想法;如果你不小心通过BookAuthor 定义了关系,没有害处,没有犯规......因为订阅机制只是悄悄地跳过重复(为了确定我测试了这个案例):

public class Book : Tag<string>
{
  public Book( string name ) : base( name ) { }

  public void AddAuthor( Author author )
  {
    SubscribeToTag( author );
    author.SubscribeToTag( this );
  }

  public IEnumerable<Author> Authors
  {
    get
    {
      return
        observables
        .Where( o => o.Tag is Author )
        .Select( o => ( Author ) o.Tag );
    }
  }

  public override void OnNext( ITag value )
  {
    switch ( value.TagType.Name )
    {
      case nameof( Author ):
        Console.WriteLine( $"{( ( Author ) value ).CurrentValue} happened to {CurrentValue}" );
        break;
    }
  }
}

...最后,一个小测试工具,看看它是否有效:

var book = new Book( "Pride and..." );
var author = new Author( "Jane Doe" );

book.AddAuthor( author );

Console.WriteLine( "\nbook's authors..." );
foreach ( var writer in book.Authors )
{
  Console.WriteLine( writer.Value );
}

Console.WriteLine( "\nauthor's books..." );
foreach ( var tome in author.Books )
{
  Console.WriteLine( tome.Value );
}

author.AddBook( book ); //--> maybe an error

Console.WriteLine( "\nbook's authors..." );
foreach ( var writer in book.Authors )
{
  Console.WriteLine( writer.Value );
}

Console.WriteLine( "\nauthor's books..." );
foreach ( var tome in author.Books )
{
  Console.WriteLine( tome.Value );
}

...吐出了这个:

Jane Doe happened to Pride and...
Pride and... happened to Jane Doe

book's authors...
Jane Doe

author's books...
Pride and...

book's authors...
Jane Doe

author's books...
Pride and...

虽然我的列表属性为IEnumerable&lt;T&gt;,但您可以将它们设为延迟加载的列表。您需要能够使列表的后备存储无效,但这可能会很自然地从您的 observables 中流出。

有数百种方法可以解决所有这些问题。我尽量不被带走。不知道...需要进行一些测试才能弄清楚这有多实用...但是想想确实很有趣。

编辑

我忘记说明的东西……书签。我猜书签的值是可更新的页码?比如:

public class Bookmark : Tag<int>
{
  public Bookmark( Book book, int pageNumber ) : base( pageNumber, false )
  {
    SubscribeToTag( book );
    book.SubscribeToTag( this );
  }

  public Book Book
  {
    get
    {
      return
        observables
        .Where( o => o.Tag is Book )
        .Select( o => o.Tag as Book )
        .FirstOrDefault( ); //--> could be .First( ) if you null-check book in ctor
    }
  }
}

那么,Book 可能具有 IEnumerable&lt;Bookmark&gt; 属性:

public class Book : Tag<string>
{
  //--> omitted stuff... <--//

  public IEnumerable<Bookmark> Bookmarks
  {
    get
    {
      return
        observables
        .Where( o => o.Tag is Bookmark )
        .Select( o => ( Bookmark ) o.Tag );
    }
  }

  //--> omitted stuff... <--//
}

关于这一点的巧妙之处在于,作者的书签就是他们书籍的书签:

public class Author : Tag<string>
{
   //--> omitted stuff... <--//

   public IEnumerable<Bookmark> Bookmarks => Books.SelectMany( b => b.Bookmarks );

   //--> omitted stuff... <--//
}

对于 yuks,我让书签拿了一本关于建筑的书……只是为了说明一种不同的方法。根据需要混合和匹配;-) 请注意,书签没有书籍列表......只有一本书......因为它更适合模型。有趣的是,您可以从单个书签中解析所有书籍书签:

var bookmarks = new List<Bookmark>( bookmark.Book.Bookmarks );

...同样轻松获取所有作者的书签:

var authBookmarks = new List<Bookmark>( bookmark.Book.Authors.SelectMany( a=> a.Bookmarks ) );

【讨论】:

  • 所以如果我们要添加var language = new Language("English"),那么我将不得不添加book.AddLanguage(language)author.AddLanguage(language)
  • 嗯...不一定。您可以通过 SubscribeToTag 方法临时添加任何标签。你不一定有一个强类型的方式来询问它......但你可以以任何看起来合适的方式公开你的枚举。
  • AddAuthorAddBook 我只是为了说明而投入的。它的架构非常松散。你可以收紧你想要的。例如,您可以创建一个虚拟的ReadRelations 方法,该方法提供了添加标签的唯一方法(如果需要)。基本类型中的强类型可枚举属性在运行时可能会也可能不会找到匹配的标签。无害无犯。没有硬性规定,除非你强加它们。
  • 我添加了一些...只是为了说明书签及其对模型的可能影响。
  • 我想试试这个。你应该找个时间来 WPF 聊天频道。有你在就好了。chat.stackoverflow.com/rooms/18165/wpf
【解决方案2】:

使用 Book 作为事实来源不是一个好方法吗?

将您当前的设置想象成一个关系数据库架构,其中除了Book 之外的所有表都没有指向其他任何内容的外键引用。您总是必须扫描Book 表来查找包含书籍的任何关系。在您给出的示例中,您必须遍历整个书籍集合才能找到由单个作者创建的所有书籍。如果您有反向引用,您只需找到单个作者,然后查看其Books 属性。

您目前如何获得未写过任何书籍的作者列表?您必须扫描书籍列表以获取确实拥有一本书的每个作者的列表,然后在该列表中找到的每个作者。

如何在不让每个作者或书签访问全局项目集合的情况下将此类信息公开给每个非图书标签类型(这感觉像是一个很大的禁忌),或者为每个标签维护所有相关标签的列表标签类型(感觉真的很痛苦低效)?

您将需要代表每个项目上每个标签类型的属性——这真的没有办法解决。如果您希望根据每个项目拥有的书签数量对列表中的项目进行排序,那么每个项目都需要提供其拥有的书签数量。

但属性不必由预先计算的列表支持。它们可以有效地指示如何进行适当的连接以获取所需信息。例如,AuthorBookmarks 属性将使用 Books 属性来获取书签列表:

public IEnumerable<Bookmark> Bookmarks => this.Books.SelectMany(b => b.Bookmarks);

如果需要,您还可以缓存结果。

如果您选择继续不让任何实体引用回到Book,而是在您的模型类中提供MyItems,您可以对指向Book 的关系做同样的事情。例如在Author:

public IEnumerable<Book> Books => MyItems.OfType<Book>.Where(b => b.Authors.Contains(this));

不过,我不建议这样做,因为您认为它感觉不对。它将模型的实现链接到一个单独的、不相关的数据结构。我的建议是实现与列表的直接关系,并为您想要排序的所有其他内容使用计算属性。

【讨论】:

  • 谢谢。自从提出这个问题以来,这是我自己使用的一个好方法。它确实工作得很好。希望我有时间回到我的项目并详细说明在走这条路线时哪些有效,哪些无效,这基本上是基于相关类型的预先计算和实时集合的混合。我遇到的一个问题是父集合将看不到已经计算的集合的变化,这让我在整个地方都有大量的事件监听器来对我的列表进行排序
  • 问题:你建议我镜像所有的关系吗?例如。 Book 是否应该填充Author 的实时列表,以及我所有的Authors 是否应该填充他们书籍的实时列表?理想情况下,我只会为每个对象使用一个主视图模型,以便随处可见更新名称或描述等更改并保持同步。
  • 我确实建议这样做,因为这些关系是您的数据模型的重要组成部分,尽管如果您真的不需要它来做任何事情,我认为将其排除在外是没有问题的。我不认为我理解您关于“每个对象一个主视图模型”的评论,或者这将如何防止这种情况发生。关于事件监听器,是的,事情就是这样:您需要发出和响应事件以触发可能发生变化的模型中的刷新。
  • 您是否尝试过为此使用实体框架?它旨在解决所有这些类型的问题。您可能不想在最终实现中使用它,但看看它是如何工作的可能会给您一些想法/灵感。
【解决方案3】:

在这种情况下,我会为书籍、作者甚至书签使用 ID。 例如,书籍/作者之间的任何关系都可以通过具有作者 ID 的书籍和具有书籍 ID 的作者来捕获。它还保证书籍/作者将是独一无二的。

为什么你觉得有必要让 Book、Author 和 Bookmark 类继承自同一个基类?是否有您想要使用的共享功能?

对于您正在寻找的功能,我想说一些扩展方法可能非常有用,例如

int GetWrittenBooks(this Author author)
{
    //either query your persistent storage or look it up in memory
}

我会说,请确保不要在类中添加太多功能。例如,您的 Book 类对可能的作者生日没有任何责任。如果 Author 的生日在 Author 类中,则 Book 不应该访问作者的生日,也不应该有 Authors,而只是对作者的引用。这本书只会对它的作者“感兴趣”,仅此而已。

对于作者也是如此:例如,它与 Book x 的第 150 页上的字母数量没有任何关系。这是本书的责任,与作者无关。

tl;博士:Single responsibility principle/Separation of concerns.

【讨论】:

  • 是的。我可以通过 ids 跟踪书籍作者,并通过 ids 跟踪作者书籍。但是当你添加更多标签时,这个过程变得非常麻烦。此外,现在单独使用 ids 确实允许 wpf 数据绑定。视图模型需要与其他视图模型的关系。我无法查看 Author 并让 wpf 数据绑定引擎以某种方式使用我的自定义 GetWrittenBooks 函数。再次想象一下,随着标签类型的增多,复杂性会如何增加。
【解决方案4】:

基本上,您可以像使用任何数据库一样使用标准实体模型方法来解决此问题,只是在您的情况下,您的数据库是在内存中的。

public class Repository
{
    public List<Book> GetBooksByAuthor(Author author) {} // books by author
    public List<Author> GetAuthors() {}
    // ...
}

这里的另一个好处是您可以以更有效的方式实现每个查找。 如果您想稍后将其换成数据库,您可以引入Repository 的接口。

每个实体都可以保留对存储库的引用,因此您可以在实体级别提供别名以方便使用:

public class Author
{
    private Repository repo;
    public Author(Repository repo)
    {
        this.repo = repo;
    }
    // ... 
    public List<Book> Books => this.repo.GetBooksByAuthor(this);
}

根据数据完整性的重要性,您还可以选择其他形式的集合,这将防止修改查询,或保存每个实体的更改列表等。

如果它是一个内部库,则可以消除许多此类问题以简化代码。

【讨论】:

  • 问题清楚地表明它是一个统一的列表。我不能为每种类型的项目设置单独的列表。我也对涉及创建数十到数百个Get X By Y 函数的解决方案不感兴趣
猜你喜欢
  • 1970-01-01
  • 2015-02-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多