Windows窗体,设计器和单例

时间:2013-05-10 16:18:42

标签: c# winforms

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

My FormMain:

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的构造函数是公共程序运行,但是当我将其更改为private时,我现在在FormMain.Designer.cs中收到错误,说“'Adventurers_of_Wintercrest.UserControls.MainScreen.MainScreen()'由于其无法访问保护等级“。它指向这一行:

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

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

3 个答案:

答案 0 :(得分:3)

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

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

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

答案 1 :(得分:1)

我想我之前回答过一个关于此的问题,看起来这就是让你从这条路线开始的原因。第一点是我没有具体推荐这种模式,只是试图教你更多关于软件开发人员如何管理范围。

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

但是

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

您可以通过让GUI在认为需要新数据(简单功能)时询问库问题,或者在需要更改某些内容时通知GUI来执行此操作。任何一种方法都比让图书馆直接摆弄标签更好。

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

修改

所以,我根据你的评论嘲笑了一个可能的替代架构的快速演示。

我的项目中有以下内容:

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

目前我没有使用DateLoadSave

FormMain只会在其上放置每个UserControl的实例。没有特殊代码。

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

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

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

因此,通过介绍,这里是代码。

GameModel:

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

需要注意的事项:您的模型有一个事件(在这种情况下,只是一个类型为EventHandler;您可以在以后创建更具表现力的事件类型,但让我们开始简单)称为DateChanged。只要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可能需要在游戏状态下执行的所有高级操作:加载或保存一场比赛,结束转弯等等。

现在其余的很容易。

TitleScreen:

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

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

InGameMenu:

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

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

注意这两个除了在Controller上调用方法之外什么都不做。容易。

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事件。这样,可以在日期递增时通知它。我还在一个按钮中实现了另一个游戏功能(结束回合)。

如果你复制并运行它,你会发现游戏日期是从很多地方操纵的,并且标签总是正确更新。最重要的是,您的控制器和模型实际上并不了解View的任何内容 - 甚至它基于WinForms。您可以像在Windows Phone或Mono上下文中那样轻松地使用这两个类。

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

答案 2 :(得分:0)

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

在这里查看示例代码: 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表单窗口,请不要再做了。就这么简单。