65.9K
CodeProject 正在变化。 阅读更多。
Home

处理对公共数据集的多个视图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (4投票s)

2001 年 5 月 21 日

13分钟阅读

viewsIcon

91911

downloadIcon

927

本文档探讨了在 C# 和 .NET 框架中使用单个数据集的多个视图时遇到的一些问题和解决方案。

摘要

本文档旨在探讨使用常见数据集的多个视图时遇到的一些问题,例如 MFC 类库中发现的文档/视图架构。这更像是一个“概念验证”研究,而不是一个功能齐全的应用程序或工具,用于研究该主题的不同方法。

引言

C# 和 .NET 框架不提供许多 C++/MFC 程序员熟悉和喜爱的内置文档/视图风格的体系结构。相反,它们将实现自己的方案来操作和管理单个或公共数据集上的多个视图,这对于每个需要这种功能的新项目都必须一遍又一遍地复制。

我不知道其他人是如何解决这类问题的,因为在互联网上搜索 C# 和 .NET 编码网站和新闻组数小时后,我发现没有任何接近这个主题的内容,并且向许多新闻组发送的消息都没有得到回应。这是因为程序员认为这非常明显且无需解释吗?是因为还没有人来做这件事吗?我不知道,所以我决定对此进行研究。希望本文能对开发社区有所帮助。

深入研究

好的,让我们来处理手头的问题。您有一个定义和维护特定数据集的应用程序。该应用程序允许对数据集进行多个视图。MFC 使用主应用程序上的 DocumentTemplate 类提供了一种非常简单轻松的方式。可以为多种视图类型以及多种文档类型定义多个模板。然后,默认的 MFC 窗口消息会为您处理所有“繁重工作”和“簿记”。实际上,实例化代码看起来像这样

// Register the application's document templates.  Document templates
//  serve as the connection between documents, frame windows and views.

CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
    IDR_MYDOCTYPE,
    RUNTIME_CLASS(CMyDoc),
    RUNTIME_CLASS(CMDIFrame),
    RUNTIME_CLASS(CMyView));

AddDocTemplate(pDocTemplate);

在开发人员没有任何进一步干预的情况下,MFC 框架将处理应用程序的 MDI 需求,在这种情况下,单个 CMyDoc 实例上的多个 CMyView 实例。然而,当前的问题是:“它实际上在做什么,以及如何在我的 C# 应用程序中提供这种功能?”

文档模板基本上是应用程序用来存储和管理应用程序应该处理的不同文档的容器。文档模板会创建一个托管列表,其中包含所有与模板匹配的文档实例。文档本身包含一个附加到它的视图的托管列表,而视图包含一个指向其数据的文档的引用。当文档被修改时,文档会调用一个方法,该方法会遍历附加到它的所有视图,并调用一个方法,该方法基本上会通知每个视图检查并重新显示文档中包含的数据。

“我”是关键

我们需要用我们的 C# 类提供类似的功能。这里的目的是不封装 MFC 提供的全部功能,而是打下基础并开始。考虑到这一点,我们在演示中寻找什么?嗯,文档需要一种方式来存储希望反映其信息的视图列表,以及一些用于操作和管理这些视图的成员方法。视图需要一个成员方法来响应文档的更新。此外,一种获取和设置视图文档的方法也很方便。

那么,现在来处理代码并开始将这些东西放进去,我们的选择是什么?我们可以提供一个基类,并强制我们的数据集和视图类从中派生。这提供了可重用性和可靠性,这是好的,但它会在某些方面限制我们吗?也许,特别是 since C# 不支持多重继承。我认为这些方法的实现足够简单,不会引起任何问题,但如果用户有某些高级、专门的或第三方类,他们真的想使用呢?如果开发人员无法访问他/她想使用的类的源代码,并且,也许数据集/视图方案的代码在一个库中,源代码也无法访问,那该怎么办?

也许,然后我们会看看使用 C# Interface。这实际上并没有提供我们理想中想要的重用性,但它提供了更大的灵活性,而无需强制用户使用基类。此外,我想稍微玩弄一下接口,这是我的文章。 :) 接口可能看起来像这样。

public interface IDocument
{
    void AddView ( IView view );
    void CloseView ( IView view );
    void CloseAllViews ( );
    void UpdateAllViews ( );
}


public interface IView
{
    void AddDocument ( IDocument doc );
    IDocument GetDocument ( );
    void UpdateData ( );
}

使用时,编译器将强制用户实现接口中定义的方法。实现非常简单。让我们定义一个数据集类,我们称之为 DataSet,它实现了 IDocument 接口。

public class DataSet : IDocument
{
     .
     .
     .
}

我们需要做的第一件事是在 DataSet 类上定义一个托管视图列表。幸运的是,C# 和 .NET 框架提供的丰富类正好提供了 System.Collections.ArrayList 类。

// the data set's managed view list.  This list is needed so that the
// data set knows who to communicate stuff to.
private System.Collections.ArrayList viewList;

AddViewCloseView 方法非常简单,它们只是像这样在 DataSet 的托管视图列表中插入或删除一个视图类。

// method needed to add a view to the managed view list
public void AddView (IView view)
{
    // If the view is now already being managed by the data set
    if ( !this.viewList.Contains (view) )
    {
        // then add this view to the list of all views managed by the data set
        this.viewList.Add (view);
    }
}

 // method to remove a view from the data set managed views list
 public void CloseView (IView view)
 {
    this.viewList.Remove (view);
 }

CloseAllViewsUpdateAllViews 也相当简单。它们必须遍历附加到数据集的所有视图。

// method to remove and close all views attached to this data set
public void CloseAllViews ()
{
    // cycle thru each view
    foreach (ViewForm view in this.viewList )
    {
        // otherwise, close the view
        view.Close ();
        // and dispose of it
        view.Dispose ();
    }

    this.viewList.Clear ();
}



// method to trigger an update on all views attached to the managed view list
public void UpdateAllViews ()
{
    // cycle thru each view
    foreach (IView view in this.viewList )
    {
        // trigger the update data method, which all views implementing the
        // IView interface will define.
        view.UpdateData ();
    }
}

信不信由你,我们已经要遇到麻烦了。从来没有人说我是最聪明的人,所以也许你已经发现了我们将要面临的一个更重要的问题,并且将在几分钟内概述。通过查看代码,你可能已经猜到了视图类的名称,所以我们先来看一下。

ViewForm 类实现了 IView 接口。方法,再次,非常简单,看起来像

// method to attach the document reference to this view
public void AddDocument ( IDocument doc )
{
    if (this.m_DataSet != null && this.m_DataSet != doc)
    {
        System.WinForms.MessageBox.Show ("Error:  Attempting to attach a data set to a form "
                                         + "which already contains a data set." );
            return;
    }

    this.m_DataSet = doc;
}

// method to return the data set associated to this view.
public IDocument GetDocument ( )
{
    return this.m_DataSet;
}

// this method is what gets called by the data set's managed view list when the data set has changed
public void UpdateData ( )
{
    System.Console.WriteLine ("Update the view: " + this.Text );
}

除了接口方法的实现外,DataSetViewForm 类的构造函数也需要进行修改。DataSet 需要知道将包含数据集及其 MDI 视图的主框架/窗口。同样,ViewForm 构造函数需要知道父窗口/框架以及它应该显示的数据集。

// constructor for the DataSet class
public DataSet( System.WinForms.Form parentForm )

// constructor for the ViewForm class
public ViewForm(IDocument doc, System.WinForms.Form parentForm )

接口的好处是,它们像基类一样,你可以使用它们来定义方法参数和变量。任何实现该接口的类都可以用于或分配给该变量或参数。这使得代码可以以稍微更通用的方式编写,例如之前的构造函数和列表迭代。

再次,请记住,这更像是一个“概念验证”,而不是一个完整的应用程序,还有一些事情也应该解决,我不会在本文的范围内处理。额外的簿记,例如处理 Frame Close 事件和在关闭所有视图后单独处理数据集,留给读者作为练习。

到目前为止一切顺利吗?很简单,对吧?那么有什么大不了的?让我们把事情搞砸一点。假设应用程序的要求需要一个额外的视图,对同一数据集的不同表示,比如树视图。一个例子可能是绘画软件包,它使用图层。前面讨论的 FormViews 可能代表作品的图形表示,显示每个单独图层的内容或总的复合图像。然后,树视图可以将每个图层呈现为一个节点,其中每个图层中的图形元素作为节点的叶子。那么,树视图的一般特征是什么?

  1. 当文档关闭时,它通常不会被处置。
  2. 很少(如果有的话)为不同项目创建相同类型的树视图,同一个树视图将被重置以显示新数据。
  3. 拆分式对接(超出本文范围)

这并没有列出树视图的所有典型特征,只是足以说明这与我们迄今为止所做的非常不同,但我们仍然希望它使用 IView 接口,因为它需要知道数据集是什么,以及何时数据集发生变化而视图需要更新。

树视图的实现将与 ViewForm 的实现几乎相同,实现接口方法并进行构造函数修改。问题出在数据集。数据集的 UpdateAllViews 方法使用 IView 作为基类,它实现了视图触发更新所期望的方法,这不是问题,实际上,这是使用接口方法的一个很好的特性。问题在于 CloseAllViews 方法。此方法做出了两个错误的假设,首先是附加到数据集的所有视图都将是 WinForm.Form 基类(不太可能但有可能),其次是附加到数据集的所有视图都希望在数据集关闭时关闭。这基本上给我们留下了两个选择,我们可以向接口添加一个关闭视图的方法,以便实现 IView 接口的所有视图都可以利用该方法并根据需要进行覆盖。第二个选择是专门化数据集的方法,以根据视图进行筛选和更改行为。因为我想看看如何检查对象的运行时类,所以我采用了这种方法,正确的做法是添加新的接口方法。

幸运的是,在 C# 中很容易检查和比较对象的运行时类。每个对象都包含一个名为 GetType 的方法,C# 提供了一个名为 typeof 的操作,该操作将基于传递的类类型创建一个新的 Type 实例。这使得类类型比较变得容易。

if ( view.GetType() != typeof(TreeForm) )

将此比较添加到 CloseAllViews 方法,并将迭代更改为使用 IView 接口类,然后将视图强制转换为 WinForm.Form(仍然是错误的假设)来关闭和处置视图将完成修改,并为我们提供所需行为。有关此“概念验证”项目的完整源代码示例,请参阅“Interface”项目。

它完成了工作,但有没有更好的方法?

更好的解决方案

除了接口概念之外,C# 还提供了委托和事件,这看起来很有前途。也许它们可以用来提供类似的行为,让我们来看看。

首先,让我们回顾一下,弄清楚我们想要完成什么。看看数据集,它有什么方法,它的任务或目标是什么?

AddView 将视图添加到 DataSet 的托管视图列表中。 这样做是为了让数据集在未来需要时能够与相应的视图进行通信。
CloseView 从 DataSet 的托管视图列表中移除视图。 这作为数据集上的簿记项是必要的,以便托管列表仅包含有效的视图。
CloseAllViews 移除并关闭与此 DataSet 相关的所有视图。 这作为数据集上的簿记项是必要的,以便在文档关闭时,所有依赖于此文档的视图都可以适当地进行清理。
UpdateAllViews 触发视图重绘其数据 指示 DataSet 托管视图列表中的所有视图更新其信息显示。

有三件事对我来说很突出:需要一个需要管理的视图列表、一个注册视图的方法以及一个移除单个视图的方法。如果能有更好的方法呢?毕竟,为什么数据集要担心哪些视图在查看它的信息?为什么数据集会关心视图正在关闭?

从数据集的角度来看,与系统的其余部分通信真正需要的是什么?简而言之,为了我们的目的,数据集需要传达它已被更改并且即将被销毁。如果数据集可以为这两种情况定义一个事件,并让任何“收听”该事件的人来处理呢?

C# 提供了两个非常适合这种情况的机制:委托和事件。委托本质上是方法原型,它提供了方法的签名,但没有提供任何功能。事件是一种对象通知“发生了重要事情,任何关心的人都可以知道”的方式。C# 方法不是将消息推入系统消息队列,而是基于“发布-订阅”模式。对于那些不知道这意味着什么的人来说,这很简单。一个对象基本上发布它想谈论的内容,其他对象订阅第一个对象发布的内容。收音机、电视和新闻组都是这种“发布-订阅”模式的典型例子,它们不断地“发布”或广播信息供大家观看,但只有那些“订阅”或收听广播的人才能真正接收到信息。

希望到目前为止你已经明白了我的意思。如果数据集不必管理“注册”的视图,而是数据集简单地定义一个委托/事件组合来通知任何关心它何时关闭以及何时已更新的人,代码会是什么样的?

// define two delegate for the close and update events
public delegate void Closing ( Object obj, EventArgs e );
public delegate void Update  ( Object obj, EventArgs e );

// define the close and update data events.
public event Closing onCloseData;
public event Update  onUpdateData;

现在,数据集只需要实现两个方法来触发两个定义的事件,这非常简单,可能看起来像

// method to trigger the data set destruction and 
// notify those who care
public void CloseData ()
{
    if ( onCloseData != null)
    {
        // instead of passing null into the second
        // parameter, add information which the "listeners"
        // might find useful
        onCloseData (this, null);
    }
}

// method to trigger the update data event on the data set.
// This method will inform all those who care that data has
// changed.
public void UpdateData ()
{
    if ( onUpdateData != null )
    {
        // instead of passing null into the second
        // parameter, add information which the "listeners"
        // might find useful
        onUpdateData (this, null);
    }
}

现在由视图类负责实现一个具有与数据集中定义的委托相同签名的方法,在我们的示例中,我们将它们定义为如下

// method used to act as the data set's update data event
// listener this method it what get's called when the data
// set triggers an Update event.
public void UpdateData (object obj, System.EventArgs e )
{
    System.Console.WriteLine ("Update the view: " + this.Text );
}

// method used to act as teh data set's close data event
// listener this method is what get's calles when the
// dataset triggers a Closing event.
public void CloseView ( Object obj, System.EventArgs e )
{
    System.Console.WriteLine ("Close the view: " + this.Text );
    this.Close ();
    this.Dispose ();
}

定义了委托后,我们需要“订阅”或“收听”数据集的消息。类构造函数是进行此操作的理想位置,可能看起来像

// set the event listeners
doc.onCloseData  += new DataSet.Closing (this.CloseView);
doc.onUpdateData += new DataSet.Update  (this.UpdateData);

看看 TreeForm 类,它迫使我们在接口示例中进行新的考虑和更改代码,实现委托/事件方案的步骤与 ViewForm 类相同。所以现在 TreeFormViewForm 类实例的行为符合我们的期望,并且当数据集更改或销毁时,它们都会响应。

在我看来,这种方法要干净得多。接口方案似乎需要的簿记麻烦几乎没有,无需担心哪些视图类正在查看和表示其数据,实际上它甚至不在乎它是否是视图类。绝对任何关心足够多的类都可以收听数据集要说的话。这是一个强大的概念。

结论

这些是处理常见数据集上多个视图的唯一方法吗?可能不是。您知道数据集的样子,您了解您的团队成员熟悉的方法,您必须决定哪种方法对您和您的情况最有意义。接口方法的一个好处是,经过一定程度的努力和研究,完全有可能实现 MFC 提供的功能集,这很熟悉。明智吗?只有您能决定。鉴于委托/事件方案提供的强大功能和灵活性,我认为花费精力定义和理解所需的通信将为代码的可读性和可用性带来回报。

© . All rights reserved.