【问题标题】:Windows Forms, Designer, and SingletonWindows 窗体、设计器和单例
【发布时间】:2013-05-05 09:28:59
【问题描述】:

我正在尝试使用 Windows 窗体和用户控件,但到目前为止,这只是一个令人头疼的问题。我不能将表单或控件设为静态,因为设计师不喜欢这样,当我在表单和控件上使用 Singleton 时,设计师仍然会向我抛出错误。

我的主窗体:

public partial class FormMain : Form
{
    private static FormMain inst;

    public static FormMain Instance
    {
        get
        {
            if (inst == null || inst.IsDisposed)
                inst = new FormMain();
            return inst;
        }
    }

    private FormMain()
    {
        inst = this;
        InitializeComponent();
    }

MainScreen.cs:

public partial class MainScreen : UserControl
{
    private static MainScreen inst;

    public static MainScreen Instance
    {
        get
        {
            if (inst == null || inst.IsDisposed)
                inst = new MainScreen();
            return inst;
        }
    }

    private MainScreen()
    {
        inst = this;
        InitializeComponent();
    }

如果 MainScreen 的构造函数是公共的,则程序运行,但是当我将其更改为私有时,我现在在 FormMain.Designer.cs 中收到一个错误,说“'Adventurers_of_Wintercrest.UserControls.MainScreen.MainScreen()' 由于它的保护等级”。它指向这一行:

        this.controlMainScreen = new Adventurers_of_Wintercrest.UserControls.MainScreen();

我认为这是设计者默认创建的类的实例。我应该抛弃设计师吗?或者有没有办法解决这个问题?或者是否有另一种方法可以在不使用 Singleton 的情况下使类属性可访问(因为我似乎无法将表单或控件设为静态)?任何帮助将不胜感激。

【问题讨论】:

  • 为什么需要把 control 做成单音。这完全不合逻辑,也不打算以这种方式使用
  • 正如我在下面的评论中提到的,我真的只是在寻找一种方法来访问我的控件的公共属性和其他控件中的表单。到目前为止,我还没有找到好的方法。
  • 不能用好的方式做坏事是好事=)。控制是已完成功能的一部分,它不应该依赖(甚至知道存在)任何外部控制。如果您需要在控件之间实现一些通信,请通过事件(实现您的自定义事件)并在主窗体中订阅它们。

标签: c# winforms


【解决方案1】:

如果要访问实例化表单的公共属性,则需要保留对每个表单的每个实例的引用。

一种方法是为每种类型的表单创建一个带有静态变量的类:

class FormReferenceHolder
{
    public static Form1 form1;
    public static Form2 form2;
}

这样,您可以在实例化表单时设置静态变量,然后您可以从程序中的任何位置访问该变量。如果表单尚不存在,您可以更进一步,并使用设置表单的属性:

class FormReferenceHolder
{
    private static Form1 form1;
    public static Form1 Form1
    {
        get
        {
            if (form1 == null) form1 = new Form1(); 
            return form1 ;
        }
    }
}

...

static void Main()
{
    Application.Run(FormReferenceHolder.Form1 );
}

【讨论】:

  • 我这样做并从 FormMain 中删除了所有控件。然后我将 TitleScreen 的一个实例添加到 ReferenceHolder 类,但我不知道如何在我的表单上显示它。我尝试在 FormMain 构造函数中使用 Show、Visible 和 Enable,但它仍然没有出现。
  • 即使你得到这个工作,这不是我推荐的架构。同样,您不希望您的库代码与您在我编辑的答案中查看我的示例有关,因为我建议您的目的是一个更好的策略。
【解决方案2】:

我想我已经回答了关于这个问题的前一个问题,看起来这就是你开始走这条路的原因。第一点是我并不是专门推荐这种模式,只是想教你更多关于软件开发人员如何管理范围的知识。

也就是说,您面临的问题并非无法克服。例如,您可以通过在运行时而不是在设计时抛出异常来阻碍公共构造函数,并修改 Program.cs 以使用静态实例而不是手动构造表单。

但是。

正如我在另一个问题中所说,更好的选择是更改架构,这样您就不需要库代码来直接操作 GUI。

您可以通过让 GUI 在它认为需要新数据(简单函数)时向库提出问题或在需要更改某些内容时通知 GUI 来做到这一点。任何一种方法都比让库直接处理标签要好。

一个好的起点应该是 MVC(模型-视图-控制器)架构,我在之前的回答中提到过。不过,最好让我们更详细地了解您的高级程序结构现在是什么样子。您在系统中使用的主要类是什么(不仅仅是您目前提到的那些)?每个人的主要职责是什么,每个人住在哪里?那么我们的建议可能会更具体一些。

编辑

因此,我根据您的评论模拟了一个可能的替代架构的快速演示。

我的项目中有以下内容:

  • FormMain (Form)
  • TitleScreen (UserControl)
  • InGameMenu (UserControl)
  • MainScreen (UserControl)
  • GameController (Class)
  • GameModel (Class)

我暂时没有使用DateLoadSave

FormMain 只是将每个 UserControl 的一个实例放到上面。没有特殊代码。

GameController 是一个单例(因为您已经尝试使用此模式,我认为尝试使用它的工作版本会对您有所帮助)通过操纵模型来响应用户输入时间>。请注意:您不能直接从 GUI(这是模型视图控制器的视图部分)操作模型。它公开了一个GameModel 的实例,并且有一堆方法可以让您执行游戏操作,例如加载/保存、结束回合等。

GameModel 是存储所有游戏状态的地方。在这种情况下,这只是一个日期和一个回合计数器(好像这将是一个回合制游戏)。日期是一个字符串(在我的游戏世界中,日期以“Eschaton 23, 3834.4”的格式显示),每一轮都是一天。

为清楚起见,TitleScreen 和 InGameMenu 各只有一个按钮。理论上(不是实现),TitleScreen 允许您开始一个新游戏,而 InGameMenu 允许您加载现有游戏。

所以介绍完毕,这里是代码。

游戏模型:

public class GameModel
{
    string displayDate = "Eschaton 23, 3834.4 (default value for illustration, never actually used)";

    public GameModel()
    {
        // Initialize to 0 and then increment immediately. This is a hack to start on turn 1 and to have the game
        // date be initialized to day 1.
        incrementableDayNumber = 0;
        IncrementDate();
    }

    public void PretendToLoadAGame(string gameDate)
    {
        DisplayDate = gameDate;
        incrementableDayNumber = 1;
    }

    public string DisplayDate
    {
        get { return displayDate; }
        set
        {
            // set the internal value
            displayDate = value;

            // notify the View of the change in Date
            if (DateChanged != null)
                DateChanged(this, EventArgs.Empty);
        }
    }

    public event EventHandler DateChanged;

    // use similar techniques to handle other properties, like 


    int incrementableDayNumber;
    public void IncrementDate()
    {
        incrementableDayNumber++;
        DisplayDate = "Eschaton " + incrementableDayNumber + ", 9994.9 (from turn end)";
    }
}

注意事项:您的模型有一个名为DateChanged 的事件(在这种情况下,只是EventHandler 类型之一;您可以稍后创建更具表现力的事件类型,但让我们从简单的开始)。每当DisplayDate 更改时,都会触发此事件。当您查看属性定义时,您可以看到这是如何发生的:set 访问器(您不会从您的 GUI 调用)如果有人在听,就会引发事件。还有一些内部字段用于存储游戏状态和 GameController(不是您的 GUI)将根据需要调用的方法。

GameController 看起来像这样:

public class GameController
{
    private static GameController instance;
    public static GameController Instance
    {
        get
        {
            if (instance == null)
                instance = new GameController();

            return instance;
        }
    }

    private GameController() 
    {
        Model = new GameModel();
    }

    public void LoadSavedGame(string file) 
    {
        // set all the state as saved from file. Since this could involve initialization
        // code that could be shared with LoadNewGame, for instance, you could move this logic
        // to a method on the model. Lots of options, as usual in software development.
        Model.PretendToLoadAGame("Eschaton 93, 9776.9 (Debug: LoadSavedGame)");
    }

    public void LoadNewGame() 
    {
        Model.PretendToLoadAGame("Eschaton 12, 9772.3 (Debug: LoadNewGame)");
    }

    public void SaveGame() 
    {
        // to do 
    }

    // Increment the date
    public void EndTurn()
    {
        Model.IncrementDate();
    }

    public GameModel Model
    {
        get;
        private set;
    }
}

在顶部您会看到单例实现。然后是构造函数,它确保始终有一个模型,以及加载和保存游戏的方法。 (在这种情况下,即使加载了新游戏,我也不会更改 GameModel 的实例。原因是 GameModel 有事件,我不希望听众必须在这个简单的示例中取消连接和重新连接它们代码。您可以自行决定如何处理。)请注意,这些方法基本上实现了您的 GUI 可能需要在游戏状态上执行的所有高级操作:加载或保存游戏、结束回合等.

现在剩下的就简单了。

标题画面:

public partial class TitleScreen : UserControl
{
    public TitleScreen()
    {
        InitializeComponent();
    }

    private void btnLoadNew(object sender, EventArgs e)
    {
        GameController.Instance.LoadNewGame();
    }
}

游戏内菜单:

public partial class InGameMenu : UserControl
{
    public InGameMenu()
    {
        InitializeComponent();
    }

    private void btnLoadSaved_Click(object sender, EventArgs e)
    {
        GameController.Instance.LoadSavedGame("test");
    }
}

注意这两个除了调用控制器上的方法之外什么都不做。很简单。

public partial class MainScreen : UserControl
{

    public MainScreen()
    {
        InitializeComponent();

        GameController.Instance.Model.DateChanged += Model_DateChanged;

        lblDate.Text = GameController.Instance.Model.DisplayDate;
    }

    void Model_DateChanged(object sender, EventArgs e)
    {
        lblDate.Text = GameController.Instance.Model.DisplayDate;
    }

    void Instance_CurrentGameChanged(object sender, EventArgs e)
    {
        throw new NotImplementedException();
    }

    private void btnEndTurn_Click(object sender, EventArgs e)
    {
        GameController.Instance.EndTurn();
    }
}

这涉及更多一点,但不是很多。关键是,它连接了模型上的DateChanged 事件。这样可以在日期增加时通知它。我还在这里的一个按钮中实现了另一个游戏功能(结束回合)。

如果你复制它并运行它,你会发现游戏日期是从很多地方被操纵的,并且标签总是正确更新的。最重要的是,您的控制器和模型实际上对视图一无所知——甚至不知道它基于 WinForms。您可以在 Windows Phone 或 Mono 上下文中轻松使用这两个类。

这是否阐明了我和其他人一直试图解释的一些架构原则?

【讨论】:

  • 我现在只有 Program.cs,它只是调用 FormMain 的一个新实例。 FormMain 包含用户控件“TitleScreen”、“MainScreen”和“InGameMenu”。全部由设计师打造。我有“日期”和“保存加载”类。我在 MainScreen 中有一个标签“lblDate”。当我从 TitleScreen 或 InGameMenu 加载游戏时,我需要相应地修改 lblDate 的 Text 属性。不幸的是,我还没有找到一种方法来做到这一点。我看到 Oleksandr 建议使用事件,但在这种情况下我不知道如何。
  • 好的,这有帮助。因此,在加载新游戏时设置 lblDate,这可能在加载 MainScreen 时发生(我猜来自 TitleScreen?)或通过游戏内菜单,用户将使用该菜单加载不同的游戏或保存的版本一个游戏或其他什么同时玩另一个游戏。我有这个权利吗?
【解决方案3】:

本质上,问题是当应用程序运行时,它会尝试实例化主窗体窗口。但是通过使用单例模式,您实际上是在禁止应用程序这样做。

看看这里的示例代码: http://msdn.microsoft.com/en-us/library/system.windows.forms.application.aspx

你会特别注意到这个部分:

[STAThread]
public static void Main()
{
    // Start the application.
    Application.Run(new Form1());
}

注意程序如何尝试实例化Form1。您的代码说,不,我真的不想要那样,因为您将构造函数标记为私有(静态表单也是如此)。但这与 Windows 窗体的工作方式背道而驰。如果你想要一个singleton form-window,就不要再做了。就这么简单。

【讨论】:

  • 我认为单例可能不是最好的方法,但我希望能够从我的表单和其他控件中的用户控件访问公共属性。最好的方法是什么?
  • 所以简短的回答是您需要创建一个应用程序“模型”。这在 WPF 中很容易做到,因为它是为此而构建的,但也可以在 win-forms 中完成。本质上,您将所有业务逻辑存储在构成“模型”的一个类或一系列类中。模型管理应用程序状态,然后在模型状态更改时向 UI 发送通知。关键本质上是将 UI 更新与实际应用程序状态分开。并将诸如按键之类的 UI 命令传递给模型,而不是在 UI 中处理它们。
  • 因此,例如,您可能有一个处理网络连接的单例类,可能还有一个用于处理与数据库的数据事务的单例类。当这些类的状态发生变化时,您会将更新推送到 UI。当用户与 UI 交互时,您将这些通知推送回模型进行处理。您不能创建单例表单的原因是因为单例等实际上是“模型”概念,而不是“视图/用户界面”概念。视图应该只响应模型中的更新,并将 UI 命令发送回模型。谷歌 MVC/MVVM/MVP。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-27
  • 2023-03-12
  • 1970-01-01
  • 2011-05-25
  • 2014-11-14
  • 1970-01-01
相关资源
最近更新 更多