c# - 构建不同类型的相关项目的集合

标签 c# wpf mvvm data-binding

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<Book> BooksAuthor类(class)。但是,当您开始添加更多类别(例如 GenrePublisherLanguage 等)时,这会失控

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

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

一个作者拥有的书签数应该是他所有书的所有书签数的总和。

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

在我的实际应用中,我已经解决了问题 #1:当向下钻取到 eg.一个Author , 我找到了所有 BookMyItems与匹配的作者,然后拉出所有 Genre , Publisher , 等从书籍列表中拉出。通过这种方式,我可以根据作者提供的书籍标签显示作者拥有的所有标签。 (我在我的 ListView 模型的范围内执行此操作,因为我知道我正在向下钻取哪个项目并且可以访问主要的 MyItems 集合)

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

我如何才能将此类信息公开给每个非 Book 的人?没有给出每个标签类型 AuthorBookmark访问全局项目集合(这感觉像是一个很大的禁忌),或者为每种标签类型维护所有相关标签的列表(这感觉真的非常低效)?

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

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

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

我理解这是一个高度相关的结构。我确实有一个后备数据库,但它在数据库中的存储方式与如何构造要绑定(bind)到 ListCollectionView 的数据几乎没有关系。

最佳答案

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

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

我正在使用术语 Tag有点听你的评论。也许Base对你更有意义?无论如何,在下面,一个Tag是通知观察标签和监听可观察标签的类型。我做了 observablesTag.Subscription 的列表.通常,您只会有一个 IDisposable 的列表。实例,因为这是 observable 通常提供的全部。原因是Tag.Subscription让您发现潜在的 Tag ...以便您可以在派生类型中抓取类型列表属性的订阅(如下面的 AuthorBook 所示。)

我设置了 Tag订阅者/通知机制在没有 的情况下工作......只是为了隔离机制。我假设大多数 Tag s 会有值...但也许有异常(exception)。

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( );
      }
    }
  }
}

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

注意 protected 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会是相似的。关于相互关系的另一种想法;如果你不小心通过 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;
    }
  }
}

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

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<T> ,您可以使它们成为延迟加载的列表。您需要能够使列表的后备存储无效,但这可能会很自然地从您的可观察对象中流出。

有数百种方法可以解决所有这些问题。我尽量不忘乎所以。不知道……需要进行一些测试才能弄清楚这有多实用……但想想肯定很有趣。

编辑

我忘记说明的东西......书签。我猜书签的值(value)是一个可更新的页码?像这样的东西:

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<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... <--//
}

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

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 ) );

关于c# - 构建不同类型的相关项目的集合,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52864826/

相关文章:

c# - ItemsControl 中元素的 TranslateZoomRotateBehavior?

c# - 如何为 DbPropertyEntry 传递值?

c# - 首先使用实体​​框架代码实现通用存储库

c# - 使用 IDataErrorInfo 或任何类似的模式来传播错误消息

c# - 在带有 MVVM 的 DataTemplate 中使用窗口资源

.net - 保护 WPF 应用程序配置设置的最佳实践是什么?

c# - 在 ASP.net Identity 中,如何在登录后将用户重定向到特定的 url?

c# - 在 t4 模板中检测类继承

c# - WPF/C# - 窗口打开时关闭

c# - WPF如何访问相关控件中的ValidationResult对象?