所以我试图将一个.NET对象暴露给COM,以便在我们的一个旧版VB6应用程序中使用。我相信我已经正确设置了.NET类,因为我现在可以在VB6中使用它,但是当我的VB6应用程序关闭时,我不认为我发布了对COM对象的所有引用/崩溃。这是一个非常事件驱动的.NET对象,我们一直在看到奇怪的问题,我们的.NET对象的先前实例化会收到一个事件,用于更新实例化所述对象。
为了测试这个,我在我的.NET对象中添加了跟踪编写器。这些跟踪打印出本地秒表的Stopwatch.ElapsedMilliseconds
属性,用于跟踪.NET对象的生命周期,以及在.NET对象的每个实例化时新创建的GUID。 / p>
此对象的第一个实例化(通过VB6)如下所示:
0>>>实例化电话<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5
90>>>已输入的AgentLogin<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5
90>>>创建CTISoftphone<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5
103>>>订阅PhoneEvent<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5
106>>>订阅OnCreateCtiConf<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5
106>>>等待任务结果。 <<< 7afc50d3-c33d-4c39-a84c-132feeefefa5
1206>>>收到的事件:OnCreateCTIConf:没有消息<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5
1206>>> OnCreateCtiConf不是null,触发事件。 <<< 7afc50d3-c33d-4c39-a84c-132feeefefa5
1211>>>调用LoginAgent<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5
1219>>>取消订阅OnCreateCtiConf<<< 7afc50d3-c33d-4c39-a84c-132feeefefa5
然后,任何后续尝试都可以正常工作,或无限期挂起。如果我们对后续无限期挂起的尝试遵循相同的跟踪,则输出如下:
0>>>已输入的AgentLogin<<< 8c820520-06d4-49e3-ba41-13d0638bfbfc
0>>>创建CTISoftphone<<< 8c820520-06d4-49e3-ba41-13d0638bfbfc
4>>>订阅PhoneEvent<<< 8c820520-06d4-49e3-ba41-13d0638bfbfc
4>>>订阅OnCreateCtiConf<<< 8c820520-06d4-49e3-ba41-13d0638bfbfc
5>>>等待任务结果。 <<< 8c820520-06d4-49e3-ba41-13d0638bfbfc
70855>>>收到的事件:OnCreateCTIConf:没有消息<<<的 7afc50d3-c33d-4c39-a84c-132feeefefa5
70855>>> OnCreateCtiConf为null,无法触发事件。 <<<的 7afc50d3-c33d-4c39-a84c-132feeefefa5
如果您在第二个输出中注意到,我们希望接收的事件正在被我们的.NET对象的前一个实例捕获!
我确保在关机时尽快将我们的.NET对象设置为VB6中的Nothing
,但它似乎无法解决问题。我会说它确实似乎有点缓解,因为我们从50/50的成功/失败变成了更像75/25的东西,但这可能是我的确认偏见,所以我无法肯定地说
在阅读有关.NET / COM互操作(特别是this文章)时,我看到每种类型的对象只有一个CCW,无论有多少实例可能存在,并且参考计数器得到维护在CCW上。所以在这一点上我想知道GC是否缺少以前的实例作为准备收集,因为我们很快就会通过CCW得到我们的.NET对象的另一个实例(例如应用程序崩溃,然后立即重新打开)因此ref计数器不是0,并且由于GC只看到CCW,它不知道它背后有多个实例?我不确定这是否有意义。
我不可能是唯一一个需要确定性地发布暴露给COM的.NET对象的人,那么其他人如何做呢?
如果需要更多示例/澄清,请告诉我,我很乐意提供!
谢谢,
修改
找到一些有趣的东西。我不认为对象本身是未被收集的,但不知何故,这个问题的事件的事件处理程序是。使用WinDbg,在有此问题的测试中,我为此对象转储了堆,并找到以下内容:
0:003> !dumpheap -stat -type MyApp.ClickToDial.Phone
Statistics:
MT Count TotalSize Class Name
038492c4 1 24 MyApp.ClickToDial.Phone+<>c__DisplayClass77_0
03848b8c 1 32 MyApp.ClickToDial.Phone+OnCallReleasedHandler
03848aa8 1 32 MyApp.ClickToDial.Phone+OnCallTransferredHandler
038488e0 1 32 MyApp.ClickToDial.Phone+OnLoginAgentConfHandler
038487fc 1 32 MyApp.ClickToDial.Phone+OnLogoutAgentConfHandler
03848718 1 32 MyApp.ClickToDial.Phone+OnSendDtmfConfHandler
03848634 1 32 MyApp.ClickToDial.Phone+OnGetCtiDataConfHandler
03848550 1 32 MyApp.ClickToDial.Phone+OnInitiateConferenceConfHandler
0384846c 1 32 MyApp.ClickToDial.Phone+OnCancelConferenceConfHandler
03848388 1 32 MyApp.ClickToDial.Phone+OnMakeCallConfHandler
038482a4 1 32 MyApp.ClickToDial.Phone+OnCompleteConferenceConfHandler
038481c0 1 32 MyApp.ClickToDial.Phone+OnCallConferencedHandler
038480dc 1 32 MyApp.ClickToDial.Phone+OnErrorHandler
03847ff8 1 32 MyApp.ClickToDial.Phone+OnCallDroppedHandler
03847f14 1 32 MyApp.ClickToDial.Phone+OnCallEstablishedHandler
038489c4 3 96 MyApp.ClickToDial.Phone+OnCreateCtiConfHandler
03844ac4 1 116 MyApp.ClickToDial.Phone
Total 19 objects
如果你查看MyApp.ClickToDial.Phone+OnCreateCtiConfHandler
的项目数(此问题的中心位置),这恰好是我遇到此问题之前运行的测试次数。
我真的不确定这是怎么回事。以下是我对这一切的实施,也许更有经验的人可以看到这个问题?
这是我们实施的入口。你先打电话之前再打电话给别人。我们需要先收到OnCreateCtiConf
事件才能致电LoginAgent
。我们必须在此方法中实例化基础CTISoftphone
,而不是我们类的构造函数,因为CTISoftphone
构造函数需要代理扩展,我们在没有这些扩展时没有实例化我们的类,因此需要等待这个事件。
public bool AgentLogin(string extension, string agentId, string password)
{
writeTrace($"Entered {nameof(AgentLogin)}");
_extension = extension;
_agentId = agentId;
try
{
writeTrace($"Creating {nameof(CTISoftphone)}");
_phone = new CTISoftphone($"ext={extension},logFile={Settings.LogLocation}");
}
catch (Exception ex)
{
writeTrace($"Caught exception: {ex.GetType()} | {ex.Message}");
//this one might get lost since we haven't subscribed to CTISoftphone.PhoneEvent yet.
OnError?.Invoke($"{_wrapperLogPrefix} Unable to create CTISoftPhone. The error was: {ex.Message}");
return false;
}
writeTrace($"Subscribing to {nameof(CTISoftphone.PhoneEvent)}");
_phoneEventHandler = new CTISoftphone.PhoneEventHandler(cti_phone_event);
_phone.PhoneEvent += _phoneEventHandler;
var are = new AutoResetEvent(false);
var task = new Task(() =>
{
try
{
writeTrace("Calling LoginAgent");
_phone.LoginAgent(agentId, password);
}
catch (Exception)
{
}
});
task.ContinueWith((t) => are.Set());
var task_handler = new OnCreateCtiConfHandler(task.Start);
writeTrace("Subscribing to OnCreateCtiConf");
OnCreateCtiConf += task_handler;
writeTrace("Waiting for task result");
are.WaitOne();
writeTrace("Unsubscribing from OnCreateCtiConf");
OnCreateCtiConf -= task_handler;
return true;
}
您会注意到,实际上只有一个来自CTISoftphone
的活动,我们在上面订阅了这个活动。此事件PhoneEvent
传递一个枚举,其中包含正在发送的事件类型,我们在cti_phone_event
方法中本地处理它,该方法位于下方(OnCreateCtiConf
事件具有不同的调试语法目的)。
private void cti_phone_event(CTISoftphone.CTIEvent type, string info)
{
writeTrace($"Received event: {type} : {(string.IsNullOrEmpty(info) ? "No message" : info)}");
switch (type)
{
case CTISoftphone.CTIEvent.OnMakeCallConf:
OnMakeCallConf?.Invoke();
break;
case CTISoftphone.CTIEvent.OnCompleteConferenceConf:
OnCompleteConferenceConf?.Invoke();
break;
case CTISoftphone.CTIEvent.OnCancelConferenceConf:
OnCancelConferenceConf?.Invoke();
break;
case CTISoftphone.CTIEvent.OnCallEstablished:
OnCallEstablished?.Invoke();
break;
case CTISoftphone.CTIEvent.OnCallDropped:
OnCallDropped?.Invoke();
break;
case CTISoftphone.CTIEvent.OnCallConferenced:
OnCallConferenced?.Invoke();
break;
case CTISoftphone.CTIEvent.OnError:
OnError?.Invoke(info);
break;
case CTISoftphone.CTIEvent.OnInitiateConferenceConf:
OnInitiateConferenceConf?.Invoke();
break;
case CTISoftphone.CTIEvent.OnGetCTIDataConf:
OnGetCtiDataConf?.Invoke();
break;
case CTISoftphone.CTIEvent.OnSendDTMFConf:
OnSendDtmfConf?.Invoke();
break;
case CTISoftphone.CTIEvent.OnLogoutAgentConf:
OnLogoutAgentConf?.Invoke();
break;
case CTISoftphone.CTIEvent.OnLoginAgentConf:
OnLoginAgentConf?.Invoke();
break;
case CTISoftphone.CTIEvent.OnCreateCTIConf:
if (OnCreateCtiConf != null)
{
writeTrace("OnCreateCtiConf was not null, firing event");
OnCreateCtiConf();
}
else
{
writeTrace("OnCreateCtiConf was null, CANNOT fire event");
}
break;
case CTISoftphone.CTIEvent.OnReleaseCallConf:
OnCallReleased?.Invoke();
break;
case CTISoftphone.CTIEvent.OnCallTransferred:
OnCallTransferred?.Invoke();
break;
default:
OnError?.Invoke($"{_wrapperLogPrefix} Received unknown CTI event type: {type}");
break;
}
}
最后,我确实实施了IDisposable
来按照建议手动取消订阅事件(尽管这似乎没有什么区别)。以下是Dispose
和Phone
析构函数实现。
public void Dispose()
{
writeTrace("Dispose called");
if (_phone != null)
_phone.PhoneEvent -= _phoneEventHandler;
_phone = null;
_ctiData = null;
OnCallEstablished = null;
OnCallDropped = null;
OnError = null;
OnCallConferenced = null;
OnCompleteConferenceConf = null;
OnMakeCallConf = null;
OnCancelConferenceConf = null;
OnInitiateConferenceConf = null;
OnGetCtiDataConf = null;
OnSendDtmfConf = null;
OnLogoutAgentConf = null;
OnLoginAgentConf = null;
OnCreateCtiConf = null;
OnCallTransferred = null;
OnCallReleased = null;
}
~Phone()
{
writeTrace("Destructor called");
Dispose();
}
所以不知怎的,似乎我的活动并没有像我期待的那样被取消订阅。怎么会这样?这里还有别的东西我不在吗?
另外,作为一个额外的检查,我转储了底层CTISoftphone
类型的堆,以确保事件/对象计数在那里有意义,并且它们看起来(来自与上面的WinDbg输出相同的会话) :
0:003> !dumpheap -stat -type Avaya.APS.CTD.
Statistics:
MT Count TotalSize Class Name
03846d64 1 32 Avaya.APS.CTD.CTISoftphone+PhoneEventHandler
03846bf0 1 108 Avaya.APS.CTD.CTISoftphone
Total 2 objects
思考?再次,在此先感谢!