ntwain/NTwain/TwainSession.cs

792 lines
29 KiB
C#
Raw Normal View History

2014-04-03 07:13:15 +08:00
using System;
2014-04-03 07:01:21 +08:00
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NTwain.Triplets;
using NTwain.Data;
using NTwain.Values;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Windows.Interop;
using System.Diagnostics;
using System.Security.Permissions;
using System.IO;
using System.ComponentModel;
using System.Threading;
namespace NTwain
{
/// <summary>
/// Provides a session for working with TWAIN api in an application.
/// </summary>
2014-04-06 04:48:28 +08:00
public class TwainSession : ITwainStateInternal, IMessageFilter, INotifyPropertyChanged
2014-04-03 07:01:21 +08:00
{
/// <summary>
/// Initializes a new instance of the <see cref="TwainSession" /> class.
/// </summary>
/// <param name="appId">The app id.</param>
/// <exception cref="System.ArgumentNullException"></exception>
public TwainSession(TWIdentity appId)
{
if (appId == null) { throw new ArgumentNullException("appId"); }
2014-04-06 06:33:21 +08:00
_appId = appId;
2014-04-03 07:01:21 +08:00
State = 1;
EnforceState = true;
}
#region properties
object _callbackObj;
SynchronizationContext _syncer;
2014-04-06 06:33:21 +08:00
TWIdentity _appId;
2014-04-03 07:01:21 +08:00
/// <summary>
/// Gets the app id used for the session.
/// </summary>
/// <value>The app id.</value>
2014-04-06 06:33:21 +08:00
TWIdentity ITwainStateInternal.GetAppId() { return _appId; }
2014-04-03 07:01:21 +08:00
/// <summary>
/// Gets the source id used for the session.
/// </summary>
/// <value>The source id.</value>
2014-04-06 04:48:28 +08:00
public TWIdentity SourceId { get; private set; }
2014-04-03 07:01:21 +08:00
/// <summary>
/// Gets the current state number as defined by the TWAIN spec.
/// </summary>
/// <value>The state.</value>
2014-04-04 19:25:11 +08:00
public int State { get; private set; }
2014-04-03 07:01:21 +08:00
/// <summary>
/// Gets or sets a value indicating whether callback is used parts of source communication
/// if supported. May be required if things don't work. This does not take effect if
/// the source is already open.
/// </summary>
/// <value>
/// <c>true</c> to disable callback; otherwise, <c>false</c>.
/// </value>
public bool DisableCallback { get; set; }
/// <summary>
/// Gets or sets a value indicating whether calls to triplets will verify the current twain session state.
/// </summary>
/// <value>
/// <c>true</c> if state value is enforced; otherwise, <c>false</c>.
/// </value>
public bool EnforceState { get; set; }
DGAudio _dgAudio;
/// <summary>
/// Gets the triplet operations defined for audio data group.
/// </summary>
/// <value>The DG audio.</value>
public DGAudio DGAudio
{
get
{
if (_dgAudio == null) { _dgAudio = new DGAudio(this); }
return _dgAudio;
}
}
DGControl _dgControl;
/// <summary>
/// Gets the triplet operations defined for control data group.
/// </summary>
/// <value>The DG control.</value>
public DGControl DGControl
{
get
{
if (_dgControl == null) { _dgControl = new DGControl(this); }
return _dgControl;
}
}
DGImage _dgImage;
/// <summary>
/// Gets the triplet operations defined for image data group.
/// </summary>
/// <value>The DG image.</value>
public DGImage DGImage
{
get
{
if (_dgImage == null) { _dgImage = new DGImage(this); }
return _dgImage;
}
}
private IList<CapabilityId> _supportedCaps;
/// <summary>
/// Gets the supported caps for the current source.
/// </summary>
/// <value>
/// The supported caps.
/// </value>
public IList<CapabilityId> SupportedCaps
{
get
{
if (_supportedCaps == null && State > 3)
{
_supportedCaps = this.GetCapabilities();
}
return _supportedCaps ?? new CapabilityId[0];
}
private set
{
_supportedCaps = value;
RaisePropertyChanged("SupportedCaps");
}
}
#endregion
2014-04-04 19:25:11 +08:00
#region state transition calls
2014-04-03 07:01:21 +08:00
2014-04-06 04:48:28 +08:00
void ITwainStateInternal.ChangeState(int newState, bool notifyChange)
2014-04-03 07:01:21 +08:00
{
2014-04-04 19:25:11 +08:00
Debug.WriteLine("TWAIN State = " + newState);
State = newState;
if (notifyChange) { RaisePropertyChanged("State"); }
}
2014-04-06 04:48:28 +08:00
ICommitable ITwainStateInternal.GetPendingStateChanger(int newState)
{
return new TentativeStateCommitable(this, newState);
}
void ITwainStateInternal.ChangeSourceId(TWIdentity sourceId)
2014-04-04 19:25:11 +08:00
{
2014-04-06 04:48:28 +08:00
SourceId = sourceId;
RaisePropertyChanged("SourceId");
2014-04-03 07:01:21 +08:00
}
HandleRef _parentHandle;
/// <summary>
/// Opens the data source manager.
/// </summary>
/// <param name="handle">The handle. On Windows = points to the window handle (hWnd) that will act as the Sources
/// "parent". On Macintosh = should be a NULL value.</param>
/// <returns></returns>
public ReturnCode OpenManager(HandleRef handle)
{
Debug.WriteLine(string.Format("Thread {0}: OpenManager.", Thread.CurrentThread.ManagedThreadId));
_parentHandle = handle;
2014-04-06 06:33:21 +08:00
var rc = DGControl.Parent.OpenDsm(handle.Handle);
2014-04-03 07:01:21 +08:00
if (rc == ReturnCode.Success)
{
// if twain2 then get mem management stuff
2014-04-06 06:33:21 +08:00
if ((_appId.DataFunctionalities & DataFunctionalities.Dsm2) == DataFunctionalities.Dsm2)
2014-04-03 07:01:21 +08:00
{
TWEntryPoint entry;
rc = DGControl.EntryPoint.Get(out entry);
if (rc == ReturnCode.Success)
{
2014-04-06 04:48:28 +08:00
MemoryManager.Instance.UpdateEntryPoint(entry);
2014-04-03 07:01:21 +08:00
Debug.WriteLine("Using TWAIN2 memory functions.");
}
else
{
CloseManager();
}
}
}
return rc;
}
/// <summary>
/// Closes the data source manager.
/// </summary>
/// <returns></returns>
public ReturnCode CloseManager()
{
Debug.WriteLine(string.Format("Thread {0}: CloseManager.", Thread.CurrentThread.ManagedThreadId));
2014-04-06 06:33:21 +08:00
var rc = DGControl.Parent.CloseDsm(_parentHandle.Handle);
2014-04-03 07:01:21 +08:00
if (rc == ReturnCode.Success)
{
_parentHandle = default(HandleRef);
2014-04-06 04:48:28 +08:00
MemoryManager.Instance.UpdateEntryPoint(null);
2014-04-03 07:01:21 +08:00
}
return rc;
}
/// <summary>
/// Loads the specified source into main memory and causes its initialization.
/// </summary>
/// <param name="sourceProductName">Name of the source.</param>
/// <returns></returns>
public ReturnCode OpenSource(string sourceProductName)
{
var source = new TWIdentity();
source.ProductName = sourceProductName;
return OpenSource(source);
}
/// <summary>
/// Loads the specified Source into main memory and causes its initialization.
/// </summary>
/// <param name="sourceId">The source id.</param>
/// <returns></returns>
public ReturnCode OpenSource(TWIdentity sourceId)
{
if (sourceId == null) { throw new ArgumentNullException("sourceId"); }
Debug.WriteLine(string.Format("Thread {0}: OpenSource.", Thread.CurrentThread.ManagedThreadId));
var rc = DGControl.Identity.OpenDS(sourceId);
if (rc == ReturnCode.Success)
{
SupportedCaps = this.GetCapabilities();
// TODO: does it work?
_syncer = SynchronizationContext.Current ?? new SynchronizationContext();
if (!DisableCallback)
{
// app v2.2 or higher uses callback2
2014-04-06 06:33:21 +08:00
if (_appId.ProtocolMajor >= 2 && _appId.ProtocolMinor >= 2)
2014-04-03 07:01:21 +08:00
{
2014-04-06 06:33:21 +08:00
var cb = new TWCallback2(CallbackHandler);
2014-04-03 07:01:21 +08:00
var rc2 = DGControl.Callback2.RegisterCallback(cb);
if (rc2 == ReturnCode.Success)
{
Debug.WriteLine("Registered callback2.");
_callbackObj = cb;
}
}
else
{
2014-04-06 06:33:21 +08:00
var cb = new TWCallback(CallbackHandler);
2014-04-03 07:01:21 +08:00
var rc2 = DGControl.Callback.RegisterCallback(cb);
if (rc2 == ReturnCode.Success)
{
Debug.WriteLine("Registered callback.");
_callbackObj = cb;
}
}
}
}
return rc;
}
ReturnCode CallbackHandler(TWIdentity origin, TWIdentity dest,
DataGroups dg, DataArgumentType dat, Values.Message msg, IntPtr data)
{
2014-04-06 04:48:28 +08:00
if (origin != null && SourceId != null && origin.Id == SourceId.Id)
2014-04-03 07:01:21 +08:00
{
Debug.WriteLine(string.Format("Thread {0}: GOT TWAIN callback for msg {1}.", Thread.CurrentThread.ManagedThreadId, msg));
// spec says should handle this on the thread that enabled the DS,
// but it's already the same and doesn't work (failure + seqError) w/o jumping to another thread and back.
// My guess is the DS needs to see the Success first before letting transfer happen
// so this is an artificial delay to make it happen.
// TODO: find a better method.
ThreadPool.QueueUserWorkItem(o =>
{
_syncer.Send(blah =>
{
HandleSourceMsg(origin, dest, dg, dat, msg, data);
}, null);
}, null);
return ReturnCode.Success;
}
return ReturnCode.Failure;
}
/// <summary>
/// When an application is finished with a Source, it must formally close the session between them
/// using this operation. This is necessary in case the Source only supports connection with a single
/// application (many desktop scanners will behave this way). A Source such as this cannot be
/// accessed by other applications until its current session is terminated
/// </summary>
/// <returns></returns>
public ReturnCode CloseSource()
{
Debug.WriteLine(string.Format("Thread {0}: CloseSource.", Thread.CurrentThread.ManagedThreadId));
var rc = DGControl.Identity.CloseDS();
if (rc == ReturnCode.Success)
{
_callbackObj = null;
SupportedCaps = null;
}
return rc;
}
TWUserInterface _twui;
/// <summary>
/// Enables the source for data acquisition.
/// </summary>
/// <param name="mode">The mode.</param>
/// <param name="modal">if set to <c>true</c> any driver UI will display as modal.</param>
/// <param name="windowHandle">The window handle if modal.</param>
/// <returns></returns>
public ReturnCode EnableSource(SourceEnableMode mode, bool modal, HandleRef windowHandle)
{
Debug.WriteLine(string.Format("Thread {0}: EnableSource.", Thread.CurrentThread.ManagedThreadId));
_twui = new TWUserInterface();
_twui.ShowUI = mode == SourceEnableMode.ShowUI;
_twui.ModalUI = modal;
_twui.hParent = windowHandle.Handle;
if (mode == SourceEnableMode.ShowUIOnly)
{
return DGControl.UserInterface.EnableDSUIOnly(_twui);
}
else
{
return DGControl.UserInterface.EnableDS(_twui);
}
}
/// <summary>
/// Disables the source to end data acquisition.
/// </summary>
/// <returns></returns>
ReturnCode DisableSource()
{
Debug.WriteLine(string.Format("Thread {0}: DisableSource.", Thread.CurrentThread.ManagedThreadId));
var rc = DGControl.UserInterface.DisableDS(_twui);
if (rc == ReturnCode.Success)
{
var hand = SourceDisabled;
if (hand != null)
{
try
{
hand(this, EventArgs.Empty);
}
catch { }
}
}
return rc;
}
#endregion
#region consumer to handle
/// <summary>
/// Occurs when source has been disabled (back to state 4).
/// </summary>
public event EventHandler SourceDisabled;
/// <summary>
/// Occurs when a data transfer is ready.
/// </summary>
public event EventHandler<TransferReadyEventArgs> TransferReady;
/// <summary>
/// Occurs when the source has generated an event.
/// </summary>
public event EventHandler<DeviceEventArgs> DeviceEvent;
/// <summary>
/// Occurs when data has been transferred.
/// </summary>
public event EventHandler<DataTransferredEventArgs> DataTransferred;
private void DoTransferRoutine()
{
TWPendingXfers pending = new TWPendingXfers();
var rc = ReturnCode.Success;
do
{
2014-04-03 20:10:49 +08:00
IList<FileFormat> formats = Enumerable.Empty<FileFormat>().ToList();
2014-04-03 07:01:21 +08:00
IList<Compression> compressions = Enumerable.Empty<Compression>().ToList();
bool canDoFileXfer = this.CapGetImageXferMech().Contains(XferMech.File);
2014-04-03 20:10:49 +08:00
var curFormat = this.GetCurrentCap<FileFormat>(CapabilityId.ICapImageFileFormat);
2014-04-03 07:01:21 +08:00
var curComp = this.GetCurrentCap<Compression>(CapabilityId.ICapCompression);
TWImageInfo imgInfo;
bool skip = false;
if (DGImage.ImageInfo.Get(out imgInfo) != ReturnCode.Success)
{
// bad!
skip = true;
}
try
{
formats = this.CapGetImageFileFormat();
}
catch { }
try
{
compressions = this.CapGetCompression();
}
catch { }
// ask consumer for cancel in case of non-ui multi-page transfers
TransferReadyEventArgs args = new TransferReadyEventArgs(pending, formats, curFormat, compressions,
curComp, canDoFileXfer, imgInfo);
args.CancelCurrent = skip;
var hand = TransferReady;
if (hand != null)
{
try
{
hand(this, args);
}
catch { }
}
if (!args.CancelAll && !args.CancelCurrent)
{
2014-04-03 20:10:49 +08:00
Values.XferMech mech = this.GetCurrentCap<XferMech>(CapabilityId.ICapXferMech);
2014-04-03 07:01:21 +08:00
if (args.CanDoFileXfer && !string.IsNullOrEmpty(args.OutputFile))
{
var setXferRC = DGControl.SetupFileXfer.Set(new TWSetupFileXfer
{
FileName = args.OutputFile,
Format = args.ImageFormat
});
if (setXferRC == ReturnCode.Success)
{
mech = XferMech.File;
}
}
// I don't know how this is supposed to work so it probably doesn't
//this.CapSetImageFormat(args.ImageFormat);
//this.CapSetImageCompression(args.ImageCompression);
#region do xfer
// TODO: expose all swallowed exceptions somehow later
IntPtr dataPtr = IntPtr.Zero;
IntPtr lockedPtr = IntPtr.Zero;
string file = null;
try
{
ReturnCode xrc = ReturnCode.Cancel;
switch (mech)
{
2014-04-03 20:10:49 +08:00
case Values.XferMech.Native:
2014-04-03 07:01:21 +08:00
xrc = DGImage.ImageNativeXfer.Get(ref dataPtr);
break;
2014-04-03 20:10:49 +08:00
case Values.XferMech.File:
2014-04-03 07:01:21 +08:00
xrc = DGImage.ImageFileXfer.Get();
if (File.Exists(args.OutputFile))
{
file = args.OutputFile;
}
break;
2014-04-03 20:10:49 +08:00
case Values.XferMech.MemFile:
2014-04-03 07:01:21 +08:00
// not supported yet
//TWImageMemXfer memxfer = new TWImageMemXfer();
//xrc = DGImage.ImageMemXfer.Get(memxfer);
break;
}
if (xrc == ReturnCode.XferDone)
{
State = 7;
try
{
var dtHand = DataTransferred;
if (dtHand != null)
{
if (dataPtr != IntPtr.Zero)
{
2014-04-06 06:33:21 +08:00
lockedPtr = MemoryManager.Instance.Lock(dataPtr);
2014-04-03 07:01:21 +08:00
}
dtHand(this, new DataTransferredEventArgs(lockedPtr, file));
}
}
catch { }
}
//}
//else if (group == DataGroups.Audio)
//{
// var xrc = DGAudio.AudioNativeXfer.Get(ref dataPtr);
// if (xrc == ReturnCode.XferDone)
// {
// State = 7;
// try
// {
// var dtHand = DataTransferred;
// if (dtHand != null)
// {
2014-04-06 04:48:28 +08:00
// lockedPtr = MemoryManager.Instance.MemLock(dataPtr);
2014-04-03 07:01:21 +08:00
// dtHand(this, new DataTransferredEventArgs(lockedPtr));
// }
// }
// catch { }
// }
//}
}
finally
{
State = 6;
// data here is allocated by source so needs to use shared mem calls
if (lockedPtr != IntPtr.Zero)
{
2014-04-06 06:33:21 +08:00
MemoryManager.Instance.Unlock(lockedPtr);
2014-04-03 07:01:21 +08:00
lockedPtr = IntPtr.Zero;
}
if (dataPtr != IntPtr.Zero)
{
2014-04-06 06:33:21 +08:00
MemoryManager.Instance.Free(dataPtr);
2014-04-03 07:01:21 +08:00
dataPtr = IntPtr.Zero;
}
}
#endregion
}
if (args.CancelAll)
{
rc = DGControl.PendingXfers.Reset(pending);
if (rc == ReturnCode.Success)
{
// if audio exit here
//if (group == DataGroups.Audio)
//{
// //???
// return;
//}
}
}
else
{
rc = DGControl.PendingXfers.EndXfer(pending);
}
} while (rc == ReturnCode.Success && pending.Count != 0);
State = 5;
DisableSource();
}
#endregion
#region messaging use
ReturnCode HandleSourceMsg(TWIdentity origin, TWIdentity destination,
DataGroups dg, DataArgumentType dat, NTwain.Values.Message msg, IntPtr data)
{
Debug.WriteLine(string.Format("Thread {0}: HandleSourceMsg at state {1} with DG={2} DAT={3} MSG={4}.", Thread.CurrentThread.ManagedThreadId, State, dg, dat, msg));
ReturnCode rc = ReturnCode.Success;
switch (msg)
{
case Values.Message.XferReady:
if (State < 6)
State = 6;
// this is the meat of all twain stuff
DoTransferRoutine();
break;
case Values.Message.DeviceEvent:
TWDeviceEvent de;
rc = DGControl.DeviceEvent.Get(out de);
if (rc == ReturnCode.Success)
{
var hand = this.DeviceEvent;
if (hand != null)
{
try
{
hand(this, new DeviceEventArgs(de));
}
catch { }
}
}
break;
case Values.Message.CloseDSReq:
case Values.Message.CloseDSOK:
// even though it says closeDS it's really disable.
// dsok is sent if source is enabled with uionly
// some sources send this at other states so do a step down
if (State > 5)
{
ForceStepDown(4);
}
else if (State == 5)
{
// needs this state check since some source sends this more than once
DisableSource();
}
break;
}
return rc;
}
/// <summary>
/// Forces the stepping down of an opened source ignoring return values.
/// Used when session state and source state become out of sync.
/// </summary>
/// <param name="targetState">State of the target.</param>
public void ForceStepDown(int targetState)
{
Debug.WriteLine(string.Format("Thread {0}: ForceStepDown.", Thread.CurrentThread.ManagedThreadId));
bool origFlag = EnforceState;
EnforceState = false;
// From the twain spec
// Stepping Back Down the States
// DG_CONTROL / DAT_PENDINGXFERS / MSG_ENDXFER → state 7 to 6
// DG_CONTROL / DAT_PENDINGXFERS / MSG_RESET → state 6 to 5
// DG_CONTROL / DAT_USERINTERFACE / MSG_DISABLEDS → state 5 to 4
// DG_CONTROL / DAT_IDENTITY / MSG_CLOSEDS → state 4 to 3
// Ignore the status returns from the calls prior to the one yielding the desired state. For instance, if a
// call during scanning returns TWCC_SEQERROR and the desire is to return to state 5, then use the
// following commands.
// DG_CONTROL / DAT_PENDINGXFERS / MSG_ENDXFER → state 7 to 6
// DG_CONTROL / DAT_PENDINGXFERS / MSG_RESET → state 6 to 5
// Being sure to confirm that DG_CONTROL / DAT_PENDINGXFERS / MSG_RESET returned
// success, the return status from DG_CONTROL / DAT_PENDINGXFERS / MSG_ENDXFER may
// be ignored.
if (targetState < 7)
{
DGControl.PendingXfers.EndXfer(new TWPendingXfers());
}
if (targetState < 6)
{
DGControl.PendingXfers.Reset(new TWPendingXfers());
}
if (targetState < 5)
{
DisableSource();
}
if (targetState < 4)
{
CloseSource();
}
if (targetState < 3)
{
CloseManager();
}
EnforceState = origFlag;
}
/// <summary>
/// Handles the message from a message loop.
/// </summary>
/// <param name="msgPtr">Pointer to message structure.</param>
/// <returns>True if handled by TWAIN.</returns>
bool HandleLoopMsgEvent(ref IntPtr msgPtr)
{
TWEvent evt = new TWEvent();
evt.pEvent = msgPtr;
var rc = DGControl.Event.ProcessEvent(evt);
2014-04-03 20:10:49 +08:00
HandleSourceMsg(null, null, DataGroups.Control, DataArgumentType.Null, evt.TWMessage, IntPtr.Zero);
2014-04-03 07:01:21 +08:00
return rc == ReturnCode.DSEvent;
}
/// <summary>
/// Message loop processor for winform.
/// Use this by adding the <see cref="TwainSession"/> as an <see cref="IMessageFilter "/>.
/// </summary>
/// <param name="m">The message to be dispatched. You cannot modify this message.</param>
/// <returns>
/// true to filter the message and stop it from being dispatched; false to allow the message to continue to the next filter or control.
/// </returns>
//[EnvironmentPermissionAttribute(SecurityAction.LinkDemand)]
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
bool IMessageFilter.PreFilterMessage(ref System.Windows.Forms.Message m)
{
if (State > 3)
{
MSG winmsg = default(MSG);
winmsg.hwnd = m.HWnd;
winmsg.lParam = m.LParam;
winmsg.message = m.Msg;
winmsg.wParam = m.WParam;
IntPtr msgPtr = IntPtr.Zero;
try
{
// no need to lock for marshal alloc
//msgPtr = MemoryManager.Instance.Allocate((uint)Marshal.SizeOf(winmsg));
msgPtr = Marshal.AllocHGlobal(Marshal.SizeOf(winmsg));
2014-04-03 07:01:21 +08:00
Marshal.StructureToPtr(winmsg, msgPtr, false);
return HandleLoopMsgEvent(ref msgPtr);
}
finally
{
if (msgPtr != IntPtr.Zero)
Marshal.FreeHGlobal(msgPtr);
//MemoryManager.Instance.Free(msgPtr);
2014-04-03 07:01:21 +08:00
}
}
return false;
}
/// <summary>
/// Message loop processor for wpf.
/// Use this as the target of <see cref="HwndSourceHook"/> delegate.
/// </summary>
/// <param name="hwnd">The window handle.</param>
/// <param name="msg">The message ID.</param>
/// <param name="wParam">The message's wParam value.</param>
/// <param name="lParam">The message's lParam value.</param>
/// <param name="handled">A value that indicates whether the message was handled. Set the value to true if the message was handled; otherwise, false.</param>
/// <returns></returns>
[EnvironmentPermissionAttribute(SecurityAction.LinkDemand)]
public IntPtr PreFilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
// always pass message since it works whether there's a callback or not?
if (State > 3)// && _callbackObj == null)
{
MSG winmsg = default(MSG);
winmsg.hwnd = hwnd;
winmsg.lParam = lParam;
winmsg.message = msg;
winmsg.wParam = wParam;
IntPtr msgPtr = IntPtr.Zero;
try
{
// no need to lock for marshal alloc
//msgPtr = MemoryManager.Instance.Allocate((uint)Marshal.SizeOf(winmsg));
msgPtr = Marshal.AllocHGlobal(Marshal.SizeOf(winmsg));
2014-04-03 07:01:21 +08:00
Marshal.StructureToPtr(winmsg, msgPtr, false);
handled = HandleLoopMsgEvent(ref msgPtr);
}
finally
{
if (msgPtr != IntPtr.Zero)
Marshal.FreeHGlobal(msgPtr);
//MemoryManager.Instance.Free(msgPtr);
2014-04-03 07:01:21 +08:00
}
}
return IntPtr.Zero;
}
#endregion
#region INotifyPropertyChanged Members
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string property)
{
var hand = PropertyChanged;
if (hand != null) { hand(this, new PropertyChangedEventArgs(property)); }
}
#endregion
}
}