我。解决方案:使用通用数据源
方法一:带事件的数据源
好吧,如果是我,我可能不会尝试直接从 Form1 获取数据。相反,我会设置一个公共数据源,然后您甚至可以取消 Form2 上的计时器,并在数据进入时驱动它(如果您愿意)。 (或者您可以保留它,并按照所需的时间间隔从数据源中提取。)
应该是这样的:
数据源类
public class ExchangeCommonDataSource
{
public event EventHandler NewDataReceived;
public void FireNewDataReceieved()
{
if (NewDataReceived != null)
NewDataReceived();
}
private string mySomeData1 = "";
public string SomeData1
{
get
{
return SomeData1;
}
set
{
SomeData1 = value;
FireNewDataReceieved();
}
}
// properties for any other data
}
然后,当您打开表单时,您只需创建一个 ExchangeCommonDataSource 实例,并将其传递给两个表单。在接收数据的表单中,您需要创建一个事件处理函数,并且无论您将数据源传递到何处,都将连接该事件。
示例:接收类代码
public void HandleDataReceived(object sender, EventArgs e)
{
// display the data
DoSomethingWith(mySource.SomeData1);
// etc...
}
private ExchangeCommonDataSource mySource;
public void SetDataSource(ExchangeCommonDataSource newSource)
{
mySource = newSource;
mySource.NewDataRecieved += new EventHandler(HandleDataReceived);
}
然后,在您的第一个表单中,您只需设置所需的属性。实际上,您可以通过单独的事件处理程序或通过创建自己的派生 EventArgs 然后使用 EventHandler<ExchangeCommonEventArgs> 而不是常规事件处理程序来获得指定要加载的实际数据的通知。
示例:主表单数据访问器
public void GetDataFromExchange()
{
mySource.SomeData1 = GetSomeData1FromExchange();
}
此外,通过这种方式,您不仅可以使用这两种形式进行交流;如果您决定将其拆分为不同的表单,您可以让他们每个人都有一个数据源的副本,并且每个人都可以处理您定义的事件或新事件,并且您不会被绑定到您所在的模型重新期望彼此之间直接交流。例如,这还允许创建一个单独的类,将一些日志数据写入磁盘,或者您可以想象的任何其他内容,而无需对您现有的任何内容进行重大更改。
二。外部更新的可扩展性
调度程序基类
那么,如果您想更新以最终发送到另一个应用程序或另一台机器怎么办?
嗯,这实际上已经很好地解释了,因为您对剩下的表单没有任何依赖关系。所以,假设你想支持三种方法:initial,form to form 方法;通过命名管道发送到同一台机器上的另一个应用程序;和 TCP/IP 完全连接到另一台机器。您需要做的就是定义一个充当调度程序的类,将其连接为接收器,然后您可以连接该对象以获取来自表单的事件并将数据放置在您想要的任何位置。
定义一个抽象类或接口来执行此操作应该相当简单,然后简单地为您想要支持的任何模式派生一个类:
示例:一个概念抽象 Dispatcher 类
public class ExchangeDataDispatcher :
IDisposable
{
public ExchangeDataDispatcher(ExchangeCommonDataSource parDataSource)
{
myDataSource = parDataSource;
myDataSource.HandleDataReceived +=
new EventHandler(HandleDataReceived);
DispatcherInitialization();
}
private ExchangeCommonDataSource myDataSource;
private void HandleDataReceived(object sender, e EventArgs)
{
// here you could record statistics or whatever about the data
DispatcherHandleDataReceived(EventArgs);
}
protected abstract void DispatcherHandleDataReceived(e EventArgs);
protected abstract void DispatcherShutdown();
// significantly ripped from Microsoft's page on IDisposable
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
// Check to see if Dispose has already been called.
if(!this.disposed)
{
// If disposing equals true, dispose all managed
// and unmanaged resources.
if(disposing)
{
// call a function which can be overridden in derived
// classes
DispatcherShutdown();
}
// Note disposing has been done.
disposed = true;
}
}
}
请参阅the Microsoft page on IDisposable 了解一些出色的示例代码以及有关 IDisposable 的更多信息...
为其他通信方法派生调度程序
没有办法让表单本身从这个类派生,但没有真正的需要,因为您可以像以前一样连接。但是,作为一个简单的例子(只是名义上的,并没有真正实现协议,你真的应该考虑实现这些类型的东西的最佳方法,但我想给你一个相当全面的例子来说明它需要什么,它并不那么简单因为真正天真的版本往往是。)
示例:(非常)概念化的基于管道的调度程序
// add these to your using statments
using System.IO.Pipes;
using System.Threading;
// NOTE: take all the async stuff with a grain of salt; this should give you a
// basic idea but there's no way I've gotten it right without actually testing
// and debugging everything. See the link
// http://stackoverflow.com/questions/6710444/named-pipes-server-read-timeout
// for some information on why it has to be done this way: basically timeout
// is not supported for named pipe server streams.
public class ExchangeDataLocalMachineDispatcher :
ExchangeDataDispatcher
{
// see http://www.switchonthecode.com/tutorials/dotnet-35-adds-named-pipes-support
// for some info on named pipes in .NET
public ExchangeDataLocalMachineDispatcher(
ExchangeCommonDataSource parDataSource,
NamedPipeServerStream ServerPipe
) :
base(parDataSource)
{
myPipe = ServerPipe;
// do any extra initialization, etc. here, negotiation for instance
StartPipeThread();
}
private NamedPipeServerStream myPipe;
private ExchangeCommonDataSource myDataSource;
// assuming you have PipeMessage defined and that your handler
// fills them in.
private List<PipeMessage> myOutgoingMessages =
new List<PipeMessage>();
private Thread myPipeThread;
private bool EndPipeListener = false;
private AutoResetEvent myWaitEvent = null;
private AutoResetEvent myDataReadyToGoEvent = null;
// set this to something reasonable for the response timeout
private int WaitTimeout = 10000;
// example: at least every minute there should be data to send
private int WaitForDataToSendTimeout = 60000;
private void StartPipeThread()
{
IAsyncResult LastResult = null;
Action<IAsyncResult> WaitForResult =
(a) =>
{
LastResult = a;
myWaitEvent.Set();
}
myPipeThread = new System.Threading.ThreadStart(
() =>
{
try
{
myWaitEvent = new AutoResetEvent(false);
myPipe.BeginWaitForConnection(
WaitForResult, null
);
bool TimedOut = !myWaitEvent.WaitOne(WaitTimeout);
if (TimedOut || !LastResult.IsCompleted)
throw new Exception("Error: pipe operation error.");
while (!EndPipeListener)
{
byte[] Response = myPipe.BeginRead(
WaitForResult, null
);
myWaitEvent.WaitOne(WaitTimeout);
if (TimedOut || !LastResult.IsCompleted)
throw new Exception("Error: pipe operation error.");
// another assumed function to handle ACKs and such
HandleResponse(Response);
myWaitEvent.Set();
// now wait for data and send
bool TimedOut =
myDataReadyToGoEvent.WaitOne(WaitForDataToSendTimeout);
if (TimedOut || !LastResult.IsCompleted)
throw new Exception("Error: no data to send.");
// an assumed function that will pull the messages out of
// the outgoing message list and send them via the pipe
SendOutgoingMessages();
myDataReadyToGoEvent.Set();
}
myWaitEvent.Set();
}
finally
{
// here you can clean up any resources, for instance you need
// to dispose the wait events, you can leave the pipe for the
// DispatcherShutdown method to fire in case something else
// wants to handle the error and try again... this is all
// fairly naive and should be thought through but I wanted
// to give you some tools you can use.
// can't remember if you're supposed to use .Close
// .Dispose or both off the top of my head; I think it's
// one or the other.
myWaitEvent.Dispose();
myDataReady.Dispose();
myWaitEvent = null;
myDataReady = null;
}
}
);
}
protected PipeMessage[] ConstructEventMessage(e EventArgs)
{
// actually we're not using the event args here but I left it
// as a placeholder for if were using the derived ones.
return
PipeMessage.CreateMessagesFromData(
myDataSource.GetMessageData()
);
}
protected override void DispatcherHandleDataReceived(e EventArgs)
{
// create a packet to send out; assuming that the
// ConstructEventMessage method is defined
myOutgoingMessages.Add(ConstructEventMessage(e));
}
protected override void DispatcherShutdown()
{
// this is called from the base class in the Dispose() method
// you can destroy any remaining resources here
if (myWaitEvent != null)
{
myWaitEvent.Dispose();
}
// etc. and
myPipe.Dispose();
}
// you could theoretically override this method too: if you do, be
// sure to call base.Dispose(disposing) so that the base class can
// clean up if resources are there to be disposed.
// protected virtual void Dispose(bool disposing)
// {
// // do stuff
// base.Dispose(disposing);
// }
}
呸。请注意,我目前对 StartPipeThread 函数的长度非常不满意,我肯定会对其进行重构。
因此,您也可以为 TCP/IP 套接字或您可以想象的任何协议实现此功能,并且无需不断修改第一部分中的类即可处理所有这些。
对于那里的任何代码质量,我深表歉意;我愿意对此提出建议/更正/抨击,如果你让我知道,我会尽我所能做出更正。 :P
三。将数据放在需要的地方
完成此设置后,您需要将相同的数据传递给正在使用它的任何表单。如果您没有同时创建两个表单,那么您将需要某种方法来让每个目标都引用相同的数据源。 (注意:选项的编号绝不意味着这些是您唯一的选择!)
以下是一些这样做的选择:
选项 1:通过主表单上的属性
如果您的主窗体负责创建每个子窗体(例如,通过菜单项),则此方法适用。您只需创建一个成员变量来保存数据,并且无论您在何处创建数据,都可以在该成员中存储对它的引用。如果您有多个源实例,则可以存储它们,例如在字典中,您可以查找所需的。
示例:主窗体代码
private ExchangeCommonDataSource myData { get; set; }
// you can also store in something that lets you identify multiple
// possible data sources; in this case, you could use, say, email address
// as a lookup: myData["mickey@example.com"];
//private Dictionary<string, ExchangeCommonDataSource> myData =
// new Dictionary<string, ExchangeCommonDataSource>();
public frmMyMainForm()
{
InitializeComponent();
// ... other initialization for the main form ...
// create the data here and save it in a private member on your
// form for later; this doesn't have to be in the constructor,
// just make sure you save a reference to the source when you
// do create your first form that uses the source.
myData = new ExchangeCommonDataSource();
}
// then, in the methods that actually create your form
// e.g. if creating from a menu item, the handlers
public void FirstFormCreatorMethod()
{
frmFirstForm = new frmFirstForm(myData);
frmFirstForm.MdiParent = this;
frmFirstForm.Show();
}
public void SecondFormCreatorMethod()
{
frmSecondForm = new frmSecondForm(myData);
frmSecondForm.MdiParent = this;
frmSecondForm.Show();
}
选项 II:static 数据源上的属性
如果表单是从主表单外部创建的,则可以使用此选项,在这种情况下,您将无法访问其方法。这种方法背后的想法是,您需要一种简单的方法来查找所需的任何项目,独立于主表单本身,并且通过提供静态方法,其他数据消费者可以使用只能访问的属性自行查找源类声明,然后是某种键(如果可以有多个来源)。
示例:ExchangeCommonDataSource.cs
// a dummy source class; this is just the parts that were relevant
// to this particular discussion.
public partial class ExchangeCommonDataSource
{
public string Username { get; set; }
public string OptionalString { get; set; }
public int MailboxNumber { get; set; }
public Guid SourceGuid { get; set; }
public long BigNumber { get; set; }
// these static members provide the functionality necessary to look
// retrieve an existing source just through the class interface
// this holds the lookup of Guid -> Source for later retreival
static Dictionary<Guid, ExchangeCommonDataSource> allSources =
new Dictionary<Guid,ExchangeCommonDataSource>();
// this factory method looks up whether the source with the passed
// Guid already exists; if it does, it returns that, otherwise it
// creates the data source and adds it to the lookup table
public static ExchangeCommonDataSource GetConnection(
Guid parSourceGuid, string parUsername, long parBigNumber
)
{
// there are many issues involved with thread safety, I do not
// guarantee that I got it right here, it's to show the idea. :)
// here I'm just providing some thread safety; by placing a lock
// around the sources to prevent two separate calls to a factory
// method from each creating a source with the same Guid.
lock (allSources)
{
ExchangeCommonDataSource RetVal;
allSources.TryGetValue(parSourceGuid, out RetVal);
if (RetVal == null)
{
// using member initializer, you can do this to limit the
// number of constructors; here we only need the one
RetVal = new ExchangeCommonDataSource(parSourceGuid) {
Username = parUsername, BigNumber = parBigNumber
};
allSources.Add(parSourceGuid, RetVal);
}
return RetVal;
}
}
// this function is actually extraneous since the GetConnection
// method will either create a new or return an existing source.
// if you had need to throw an exception if GetConnection was
// called on for existing source, you could use this to retrieve
public static
ExchangeCommonDataSource LookupDatasource(Guid parSourceGuid)
{
// again locking the sources lookup for thread-safety. the
// rules: 1. don't provide external access to allSources
// 2. everywhere you use allSources in the class,
// place a lock(allsources { } block around it
lock (allSources)
{
ExchangeCommonDataSource RetVal;
allSources.TryGetValue(parSourceGuid, out RetVal);
return RetVal;
}
}
// private constructor; it is private so we can rely on the
// fact that we only provide factory method(s) that insert the
// new items into the main dictionary
private ExchangeCommonDataSource(Guid SourceGuid)
{
// if you didn't want to use a factory, you could always do
// something like the following without it; note you will
// have to throw an error with this implementation because
// there's no way to recover.
//lock (allSources)
//{
// ExchangeCommonDataSource Existing;
// ExchangeCommonDataSource.allSources.
// TryGetValue(parSourceGuid, out Existing);
// if (Existing != null)
// throw new Exception("Requested duplicate source!");
//}
// ... initialize ...
}
}
现在要访问,客户端只需要某种密钥来访问数据:
示例:frmClientClass.cs
public partial class frmClientClass
{
ExchangeCommonDataSource myDataSource = null;
public void InitializeSource(Guid parSourceGuid)
{
myDataSource = ExchangeCommonDataSource.GetConnection(parSourceGuid);
}
}
我发现这通常是比选项 1 更具吸引力的解决方案,因为任何可以访问类和 ID 的东西都可以获取数据源,并且因为它相当容易实现,并且它自动支持执行多个实例您的数据源类。
它的开销相当低,而且由于在大多数情况下,获取数据源是不会在紧密循环中完成的(如果是的话,您将拥有本地副本,而不是从字典每次)任何小的性能损失都应该值得易用。而且,最重要的是,即使您从一个数据源开始,您也可以轻松地将应用程序扩展到更多,而无需重写任何代码或进行任何进一步的工作。
例如,假设您只有一个数据源,一种非常快速的使用方法是使用一个已知值作为您的 Dictionary 键,然后您现在可以在第二个中对其进行硬编码。因此,例如,您可以将空 GUID 作为您的密钥,并将其用于您的两个表单。即主表单或您的第一个数据表单将使用 Guid.Empty 调用 create 方法来最初创建数据,然后您可以在打开第二个表单时使用它来访问它。
选项 3:“单例”模式类
好的,我不会为此花费太多时间或编写代码,但如果我不提及它,我会失职。它与选项 2 非常相似,除了使用静态 Dictionary 来查找多个数据源之外,您创建了一个类,该类的一个实例存储在静态属性中,并且您防止(通过异常)任何尝试创建更多课程。然后,将所有构造函数设置为private,如果静态变量已经包含一个对象,则让它们抛出异常,然后创建一个 getInstance() 方法,该方法返回该类的单个实例,如果它为空则创建它。
现在,您需要了解一些线程安全小技巧问题才能编写传统的单例,因此请务必了解这些问题(StackOverflow 上存在处理该问题的问题)。如果您不需要任何特定知识来构造类的实例,则可以通过简单地初始化声明它的变量来避免这些问题,例如static MyClass theInstance = new MyClass();,如果您曾经使用过,我强烈建议您这样做。
我在(相当遥远的)过去使用过单例,并不是说它们偶尔没有用处,尤其是在嵌入式系统中。但是,这不是嵌入式系统,几乎每次我在 GUI 应用程序中使用 Singleton 时,我都会后悔这样做,因为我最终将它重写为允许多个实例的东西。如果你真的只需要一个副本,你所要做的就是在使用它的类中放置一个成员变量,比如你的主窗体,并确保你只创建一个。这样做,您甚至可以通过在可以触发异常的类中设置静态标志来使用该模式;首次创建对象时将其设置为 true,然后如果为 true,则可以抛出异常。
无论如何,我个人关于何时编写单例的第一条规则是:除非您确定永远不会需要多个单例,否则不要这样做。如果它通过了那个,那么第二条规则是:你错了,它有一种可能发生的方式,所以只要把它写成一个普通的类,并以其他方式处理它的单例性。 :) 说真的,真正的规则是,除非你有非常充分的理由或从中获得显着的好处,否则不要这样做。
哦,重申一下:很可能实现单例的模式,而无需编写规范的单例类。模式很好,只要当需要该类的第二个实例时,消除该模式的成本非常低。
选项 4:单独的类
选项 4 与选项 2 非常相似,但在第二类中实现。 (事实上,如果你认为你可能有多个数据源,那么从这里开始是值得的,尽管最初设置的时间要多一点。)而不是将你的静态项作为该类的成员,实现另一个具有类似它们并提供访问权限的类。这是一种将类本身与类的创建分离的方法。例如,如果您正在编写一个库,并且您想提供几种不同类型的数据源,您可以实现一个基类,然后从基类派生您的其他对象,然后通过提供工厂的类提供创建机制创建不同种类的方法。
在这种情况下,您很可能根本不希望使用您的数据源的任何东西都必须知道有关数据源类的实现的任何信息,而只通过基本接口,这提供了一个简单的方法来做到这一点。如果您必须将其全部编写为基类静态成员,那么每次派生新类时都会强制重写基类,并且还会强制基类了解派生类的一些信息,其中每个一般来说,是要避免的。换句话说,并不是说它永远没有用,而是没有很好的理由就不要这样做,也不要在没有理解其含义的情况下这样做。
示例:外部类的代码
InfostoreBase.cs
// our data source base class; could do interface instead like:
// public interface IInfostoreBase
public abstract class InfostoreBase
{
public abstract int Information { get; set; }
public abstract string NameOfItem { get; set; }
public abstract decimal Cost { get; set; }
// ... etc ...
}
InfostoreEnterprise.cs
public class InfostoreHomeEdition :
InfostoreBase
{
public override int Information { get { /* ... */ } set { /* ... */ }}
public override string NameOfItem { get { /* ... */ } set { /* ... */ }}
public override decimal Cost { get { /* ... */ } set { /* ... */ }}
public void SetFeatures(string parSomething) { /* ... */ }
}
InfostoreHomeEdition.cs
public class InfostoreEnterpriseEdition :
InfostoreBase
{
public override int Information { get { /* ... */ } set { /* ... */ }}
public override string NameOfItem{ get { /* ... */ } set { /* ... */ }}
public override decimal Cost { get { /* ... */ } set { /* ... */ }}
public void SetBaseDiscount(decimal parSomethingElse) { /* ... */ }
}
InfostoreProvider.cs
public class InfostoreProvider
{
static Dictionary<Guid, InfostoreBase> allSources =
new Dictionary<Guid,InfostoreBase>();
public static InfostoreBase
GetHomeConnection(Guid CustomerKey, string HomeFeatures)
{
lock (allSources)
{
InfostoreBase RetVal;
if (!ValidHomeKey(CustomerKey))
throw new
InvalidKeyException("not valid for Home Edition");
allSources.TryGetValue(CustomerKey, out RetVal);
if (RetVal == null)
{
RetVal = new InfostoreHomeEdition();
allSources.Add(CustomerKey, RetVal);
}
var ActualVersion = (InfostoreHomeEdition) RetVal;
RetVal.SetFeatures(HomeFeatures);
return RetVal;
}
}
public static InfostoreBase
GetEnterpriseConnection(Guid CustomerKey, decimal BaseDiscount)
{
lock (allSources)
{
InfostoreBase RetVal;
if (!ValidEnterpriseKey(CustomerKey))
throw new
InvalidKeyException("not valid for Enterprise Edition");
allSources.TryGetValue(CustomerKey, out RetVal);
if (RetVal == null)
{
RetVal = new InfostoreHomeEdition();
allSources.Add(CustomerKey, RetVal);
}
var ActualVersion = (InfostoreEnterpriseEdition) RetVal;
RetVal.SetBaseDiscount(CostBase);
return RetVal;
}
}
}
客户端类中的代码
private InfostoreBase myConnectionSource;
private void Initialize()
{
// ...
myConnectionSource =
InfostoreProvider.GetConnection(
myKey, isEnterprise, myData
);
//...
}
结束
我认为这涵盖了很多可能的解决方案;它们都不是特别难以实施,并且每个都有自己的优点和缺点。一般来说,我会选择选项 2 或选项 4,但 [破纪录] 它始终取决于您的具体情况。我认为使用扩展这些来处理许多不同的情况会相当容易。当然,如果有任何问题,请告诉我。