构建不同类型的相关项的集合

Jul*_*ien 6 c# data-binding wpf mvvm

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);
Run Code Online (Sandbox Code Playgroud)

通过这样的设置,我可以显示包含书籍,作者和书签的单一列表.我可以深入研究一本书,并查看具体的书籍作者和书签.

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

一个简单的解决方案是为每个其他类添加适当的列表.例如. public List<Book> BooksAuthor类.然而,这种失控的,当你开始增加更多的类别(例如Genre,Publisher,Language等)

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

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

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

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

在我的实际应用中,我已经解决了问题#1:当深入研究例如.一Author,我发现所有的BookMyItems与匹配的作者,然后把所有的Genre,Publisher等从图书列表拉.通过这种方式,我可以根据书籍提供的标签显示作者拥有的所有标签.(我在列表视图模型的范围内执行此操作,因为我知道我正在深入研究哪些项目并且可以访问主MyItems集合)

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

如何在Book不给予每个非标签类型AuthorBookmark访问全局项目集合的情况下(这感觉像是一个大禁忌),或者为每个标签类型维护所有相关标签的列表(这只是感觉)真的痛苦无效)?

编辑1:您能定义"标签"和"标签类型"并提供更多示例吗?

我正在使用标签来定义我想要列入我的列表中的任何项目.A Book,an Author,a Bookmark都是标签.Language并且Genre也将是标签.一本书有作者和语言,就像一种语言有书籍和作者.

编辑2:即使您不打算拥有支持数据库,您也应该从刷新实体关系模型知识中受益

我知道这是一个高度关系的结构.我有一个支持数据库但它如何存储在数据库中与如何构造要绑定到的数据几乎没有关系ListCollectionView

Cla*_*lay 4

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

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

我使用这个词Tag是为了遵循你的评论。也许Base对你来说更有意义?不管怎样,下面的 aTag是一种通知观察标签并监听可观察标签的类型。我将其列为observables的列表Tag.Subscription。通常,您只会有一个IDisposable实例列表,因为这就是 observable 通常提供的全部内容。这样做的原因是,它Tag.Subscription可以让您发现底层Tag...以便您可以在派生类型中抓取您类型的列表属性的订阅(如下面的 aAuthor和中所示Book。)

我设置了订阅者/通知者机制,使其在没有值的情况下Tag工作......只是为了隔离该机制。我认为大多数s 都会有值......但也许也有例外。Tag

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;
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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( );
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

注意受保护的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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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 );
}
Run Code Online (Sandbox Code Playgroud)

...吐出这个:

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...
Run Code Online (Sandbox Code Playgroud)

虽然我的列表属性是IEnumerable<T>,但您可以将它们设置为延迟加载列表。您需要能够使列表的后备存储无效,但这可能会很自然地从您的可观察值中产生。

有数百种方法可以实现这一切。我努力不让自己得意忘形。不知道……需要一些测试才能弄清楚这有多实用……但想想确实很有趣。

编辑

我忘记说明的东西......书签。我猜书签的值是可更新的页码?就像是:

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
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

那么, aBook可能有一个IEnumerable<Bookmark>属性:

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... <--//
}
Run Code Online (Sandbox Code Playgroud)

巧妙的是,作者的书签就是他们书籍的书签:

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

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

   //--> omitted stuff... <--//
}
Run Code Online (Sandbox Code Playgroud)

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

var bookmarks = new List<Bookmark>( bookmark.Book.Bookmarks );
Run Code Online (Sandbox Code Playgroud)

...并且可以轻松获取所有作者的书签:

var authBookmarks = new List<Bookmark>( bookmark.Book.Authors.SelectMany( a=> a.Bookmarks ) );
Run Code Online (Sandbox Code Playgroud)