Board index » delphi » Outlook (2003): looping through item selections > 250

Outlook (2003): looping through item selections > 250


2008-06-17 11:35:01 PM
delphi147
Hi all! (Dmitry specifically, I guess)
[Using Add-in Express and Redemption in an Outlook COM Addin compiled
in Delphi 2007 Win32]
I'm trying to sum up Duration values of selected Journal items for
statistical purposes but I keep hitting the 250 item limit (i.e.
accessing more items just returns nil or errors - this is apparently a
well-known problem as I found out today) and can not figure out how to
circumvent it.
I had almost resigned to accepting that it is simply not possible when
on a whim I typed the following VBA code into OutlookSpy:
c = Selection.Count
d = 0
for i = 1 to c
d = d + Selection.Item(i).Duration
next
Debug.Print(c)
Debug.Print(d / 60)
Not only does this return the correct sum for all ~900 items in my test
journal - it does so instantly!!! My equivalent Delphi code takes about
15 seconds to return the sum for the first 249 items...
How can this be? And how can I accomplish the same thing in Delphi from
within my addin?
Here's my current implementation (called from an
Explorer.SelectionChange event handler - slightly simplified for the
purposes of this post):
for i := 1 to ASelection.Count do
if Supports(ASelection.Item(i), _JournalItem, lItem) then
lMinutes := lMinutes + lItem.Duration;
Any ideas?
Cheers,
Oliver
 
 

Re:Outlook (2003): looping through item selections > 250

Is your VBS and Delphi code runnign on the same machine using the same
profile?
If not, is the problematic profile using the online mode (which exhibits
this limitation) vs cached mode (not a subject to this restriction since no
server objects are opened)?
--
Dmitry Streblechenko (MVP)
www.dimastr.com/
OutlookSpy - Outlook, CDO
and MAPI Developer Tool
-
"Oliver Giesen" <XXXX@XXXXX.COM>writes
Quote
Hi all! (Dmitry specifically, I guess)

[Using Add-in Express and Redemption in an Outlook COM Addin compiled
in Delphi 2007 Win32]

I'm trying to sum up Duration values of selected Journal items for
statistical purposes but I keep hitting the 250 item limit (i.e.
accessing more items just returns nil or errors - this is apparently a
well-known problem as I found out today) and can not figure out how to
circumvent it.

I had almost resigned to accepting that it is simply not possible when
on a whim I typed the following VBA code into OutlookSpy:

c = Selection.Count
d = 0
for i = 1 to c
d = d + Selection.Item(i).Duration
next
Debug.Print(c)
Debug.Print(d / 60)

Not only does this return the correct sum for all ~900 items in my test
journal - it does so instantly!!! My equivalent Delphi code takes about
15 seconds to return the sum for the first 249 items...

How can this be? And how can I accomplish the same thing in Delphi from
within my addin?

Here's my current implementation (called from an
Explorer.SelectionChange event handler - slightly simplified for the
purposes of this post):

for i := 1 to ASelection.Count do
if Supports(ASelection.Item(i), _JournalItem, lItem) then
lMinutes := lMinutes + lItem.Duration;

Any ideas?

Cheers,

Oliver
 

Re:Outlook (2003): looping through item selections > 250

Dmitry Streblechenko writes:
Quote
Is your VBS and Delphi code runnign on the same machine using the
same profile? If not, is the problematic profile using the online
mode (which exhibits this limitation) vs cached mode (not a subject
to this restriction since no server objects are opened)?
Yes, this is all running inside the same Outlook instance, which is
using non-cached mode to access an Exchange 2003 server.
I'll do some more tests using an actual VBA Macro rather than using
OutlookSpy. Does OutlookSpy do anything special to execute its scripts
which might explain the vastly different behaviour?
--
Oliver
 

Re:Outlook (2003): looping through item selections > 250

Oliver Giesen writes:
Quote
I'll do some more tests using an actual VBA Macro rather than using
OutlookSpy.
Right, the following VBA macro also returns the correct value for>4000
selected items within about a second, just like the OutlookSpy-script
did:
Sub CalcTotalDuration()
Set App = CreateObject("Outlook.Application")
Set Sel = App.ActiveExplorer.Selection
D = 0
For Each itm In Sel
D = D + itm.Duration
Next
MsgBox D / 60
End Sub
Any ideas how to accomplish the same thing in Delphi code?
Actually, the fact that the VB(Visual Basic) version is so blazingly fast compared to
my Delphi implementation is almost the bigger mistery to me... it
doesn't really give the impression of going through the server at
all... (which of course shouldn't be necessary in the first place
anyway as you can see that Outlook already has all the information)
Cheers,
--
Oliver
 

Re:Outlook (2003): looping through item selections > 250

Oliver Giesen writes:
Quote
Actually, the fact that the VB(Visual Basic) version is so blazingly fast compared
to my Delphi implementation is almost the bigger mistery to me...
A little more perspective on a side-note: I copied the VBA code from my
last post into a standalone VBS file and executed that outside of
Outlook. This took about a minute or more to execute but still returned
the correct result...
That time now is much more in line with what I see in my Delphi add-in.
--
Oliver
 

Re:Outlook (2003): looping through item selections > 250

Your code uses early binding vs late bidning in VBS.
How about something like
var lItem : OleVariant;
...
for i := 1 to ASelection.Count do begin
lItem := ASelection.Item(i);
lMinutes := lMinutes + lItem.Duration;
--
Dmitry Streblechenko (MVP)
www.dimastr.com/
OutlookSpy - Outlook, CDO
and MAPI Developer Tool
-
"Oliver Giesen" <XXXX@XXXXX.COM>writes
Quote
Oliver Giesen writes:

>Actually, the fact that the VB(Visual Basic) version is so blazingly fast compared
>to my Delphi implementation is almost the bigger mistery to me...

A little more perspective on a side-note: I copied the VBA code from my
last post into a standalone VBS file and executed that outside of
Outlook. This took about a minute or more to execute but still returned
the correct result...
That time now is much more in line with what I see in my Delphi add-in.

--

Oliver
 

Re:Outlook (2003): looping through item selections > 250

"Oliver Giesen" <XXXX@XXXXX.COM>wrote in
Quote
Oliver Giesen writes:

>I'll do some more tests using an actual VBA Macro rather than using
>OutlookSpy.

Right, the following VBA macro also returns the correct value for>4000
selected items within about a second, just like the OutlookSpy-script
did:

Sub CalcTotalDuration()
Set App = CreateObject("Outlook.Application")
Set Sel = App.ActiveExplorer.Selection
D = 0
For Each itm In Sel
D = D + itm.Duration
Next
MsgBox D / 60
End Sub


Any ideas how to accomplish the same thing in Delphi code?

Actually, the fact that the VB(Visual Basic) version is so blazingly fast compared to
my Delphi implementation is almost the bigger mistery to me... it
doesn't really give the impression of going through the server at
all... (which of course shouldn't be necessary in the first place
anyway as you can see that Outlook already has all the information)

Cheers,

FWIW VB's For Each uses IEnumVARIANT not Item(I). That might account for
a difference in speed.
I use the following routine to emulate For Each
function ForEach(var Element: OleVariant; TheCollection: IEnumVariant):
Boolean;
var
Fetched: {$IFDEF Delphi6Up}Cardinal{$ELSE}Integer{$ENDIF};
hr: {$IFDEF Delphi6Up}Cardinal{$ELSE}Integer{$ENDIF};
begin
Result := False;
if TheCollection <>nil then
begin
Fetched := 0;
hr := TheCollection.Next(1, Element, {$IFDEF Delphi6Up}Fetched
{$ELSE}@Fetched{$ENDIF});
case hr of
S_OK: Result := (Fetched>0);
S_FALSE: ;
else
OleCheck(hr);
end;
end;
end;
called by
var
Enum: IEnumVARIANT;
V: OleVariant;
Enum := SomeCollection._NewEnum as IEnumVARIANT;
while ForEach(V, Enum) do
...
HTH
 

Re:Outlook (2003): looping through item selections > 250

Dmitry Streblechenko writes:
Quote
Your code uses early binding vs late bidning in VBS.

How about something like

var lItem : OleVariant;
...
for i := 1 to ASelection.Count do begin
lItem := ASelection.Item(i);
lMinutes := lMinutes + lItem.Duration;
Pffrrt... yeah, this works flawlessly (and fast, too!)... well, except
that so far I always considered late binding to be a sort of flaw by
itself... ;)
Thanks!!! :)
Do you have any idea or even an explanation why this makes the
difference here?
Are there any other cases or general rules where late binding is
preferable over early binding? So far I always avoided variants like
the plague...
Cheers,
--
Oliver
 

Re:Outlook (2003): looping through item selections > 250

Chris.Cheney writes:
Quote
FWIW VB's For Each uses IEnumVARIANT not Item(I). That might account
for a difference in speed.
I first coded the VB(Visual Basic) thing with FOR 1 TO Count and it seemed to be just
as fast. I had also been wondering how VB(Visual Basic) does the ForEach, though. I
had just tried it on a whim myself...
Quote
I use the following routine to emulate For Each

<.snip.>

Enum := SomeCollection._NewEnum as IEnumVARIANT;
while ForEach(V, Enum) do
...
Hmm, couldn't get this to work with the Selection... I get a "_NewEnum
not supported by automation object" EOleError...
Cheers,
--
Oliver
 

Re:Outlook (2003): looping through item selections > 250

"Oliver Giesen" <XXXX@XXXXX.COM>wrote in
Quote
Chris.Cheney writes:

>FWIW VB's For Each uses IEnumVARIANT not Item(I). That might account
>for a difference in speed.

I first coded the VB(Visual Basic) thing with FOR 1 TO Count and it seemed to be just
as fast. I had also been wondering how VB(Visual Basic) does the ForEach, though. I
had just tried it on a whim myself...


>I use the following routine to emulate For Each
>
<.snip.>
>
>Enum := SomeCollection._NewEnum as IEnumVARIANT;
>while ForEach(V, Enum) do
>...

Hmm, couldn't get this to work with the Selection... I get a "_NewEnum
not supported by automation object" EOleError...

Cheers,

Indeed, the OutlookXP.pas file shows Item and Count properties for the
Selection interface but not a sign of _NewEnum. Sorry to have mislead you.
(FWIW I gave up using Item in favour of IEnumVARIANT enumeration because
applications, even MS applications, were inconsistent whether Item indexing
started at 0 or 1. Looks as though I shall have to change my strategy!)
 

Re:Outlook (2003): looping through item selections > 250

In general, early binding is faster, but by not much if the code has to
retrieve anything over the network.
As for why late binding was faster, who knows? Obviously I have not seen the
Outlook source code, but I do know that OOM tries to avoid opening the
underlying MAPI object (IMessage) if you do not access any properties that
are not cached - e.g. when you loop through items in a folder, you can use
Items.SetColumns prior ot looping to tell Outlook which properties you will
be reading later. This way Outlook retrieves multiple properties from
multiple messages using the folder contents table (IMAPITable::SetColumns,
IMAPITable::QueryRows /etc) rather than opening the message
(ImsgStore::OpenEntry - expensive) and reading properties one at a time
(IMessage::GetProps).
Could be something similar with the Selection collection if Duration
property is used as one of the table columns and is already available. And
when you use early binding, Outlook might be always forced to open the
underlying IMessage.
--
Dmitry Streblechenko (MVP)
www.dimastr.com/
OutlookSpy - Outlook, CDO
and MAPI Developer Tool
-
"Oliver Giesen" <XXXX@XXXXX.COM>writes
Quote
Dmitry Streblechenko writes:

>Your code uses early binding vs late bidning in VBS.
>
>How about something like
>
>var lItem : OleVariant;
>...
>for i := 1 to ASelection.Count do begin
>lItem := ASelection.Item(i);
>lMinutes := lMinutes + lItem.Duration;

Pffrrt... yeah, this works flawlessly (and fast, too!)... well, except
that so far I always considered late binding to be a sort of flaw by
itself... ;)

Thanks!!! :)

Do you have any idea or even an explanation why this makes the
difference here?

Are there any other cases or general rules where late binding is
preferable over early binding? So far I always avoided variants like
the plague...

Cheers,

--

Oliver
 

Re:Outlook (2003): looping through item selections > 250

If _NewEnum is not explicitly exposed, it might still be available by
callign IDispatch::Invoke() with dispId = -4.
AFAIK that is what VB(Visual Basic) does, and "for each" for the Selection collection
works fine in all versions of Outlook.
--
Dmitry Streblechenko (MVP)
www.dimastr.com/
OutlookSpy - Outlook, CDO
and MAPI Developer Tool
-
"Chris.Cheney" <XXXX@XXXXX.COM>writes
Quote
"Oliver Giesen" <XXXX@XXXXX.COM>wrote in
news:485a1913$XXXX@XXXXX.COM:

>Chris.Cheney writes:
>
>>FWIW VB's For Each uses IEnumVARIANT not Item(I). That might account
>>for a difference in speed.
>
>I first coded the VB(Visual Basic) thing with FOR 1 TO Count and it seemed to be just
>as fast. I had also been wondering how VB(Visual Basic) does the ForEach, though. I
>had just tried it on a whim myself...
>
>
>>I use the following routine to emulate For Each
>>
><.snip.>
>>
>>Enum := SomeCollection._NewEnum as IEnumVARIANT;
>>while ForEach(V, Enum) do
>>...
>
>Hmm, couldn't get this to work with the Selection... I get a "_NewEnum
>not supported by automation object" EOleError...
>
>Cheers,
>

Indeed, the OutlookXP.pas file shows Item and Count properties for the
Selection interface but not a sign of _NewEnum. Sorry to have mislead you.

(FWIW I gave up using Item in favour of IEnumVARIANT enumeration because
applications, even MS applications, were inconsistent whether Item
indexing
started at 0 or 1. Looks as though I shall have to change my strategy!)
 

Re:Outlook (2003): looping through item selections > 250

"Dmitry Streblechenko" <XXXX@XXXXX.COM>wrote in news:485a8eee$1
@newsgroups.borland.com:
Quote
If _NewEnum is not explicitly exposed, it might still be available by
callign IDispatch::Invoke() with dispId = -4.
AFAIK that is what VB(Visual Basic) does, and "for each" for the Selection collection
works fine in all versions of Outlook.

Thanks for that - I had wondered what VB(Visual Basic) would do.
 

Re:Outlook (2003): looping through item selections > 250

Dmitry Streblechenko writes:
Quote
If _NewEnum is not explicitly exposed, it might still be available by
callign IDispatch::Invoke() with dispId = -4. AFAIK that is what VB
does, and "for each" for the Selection collection works fine in all
versions of Outlook.
Great! This works even on the Selection (though it does not appear to
be noticeably faster than the regular for to loop).
I have also found that now that the iteration itself has become pretty
fast (<200ms for ~3000 items) the real bottleneck is accessing the
Selection property in the first place: For the ~3000 items mentioned
before this alone takes about 1600ms... and it doesn't seem to get
faster by using late binding. Yet, when I call the essentially
identical VBA macro (see previous posts) that penalty does not seem to
apply... i.e. I get the result in less than a second.
Anyway, I have now wrapped yours and Chris' suggestions about the Foreach
iteration into an enumerator so you can use for-in loops on such
collections in Delphi (see below). Feel free to comment and/or share.
Usage would be something like:
for lItem in EnumOleCollection(OutlookApp.ActiveExplorer.Selection) do
...
Here's the code:
type
TOleCollectionEnum = class
private
FElem: OleVariant;
FEnum: IEnumVariant;
public
constructor Create(const AEnum: IEnumVARIANT);
function MoveNext: Boolean;
property Current: OleVariant read FElem;
end;
IOleCollectionEnumFactory = interface
function GetEnumerator: TOleCollectionEnum;
end;
TOleCollectionEnumFactory = class(TInterfacedObject,
IOleCollectionEnumFactory)
private
FEnum: IEnumVariant;
public
constructor Create(const ACollection: IDispatch);
function GetEnumerator: TOleCollectionEnum;
end;
function EnumOleCollection(const ACollection: IDispatch):
IOleCollectionEnumFactory;
begin
Result := TOleCollectionEnumFactory.Create(ACollection);
end;
{ TOleCollectionEnumFactory }
{ inspired by a newsgroup posting by Dmitry Streblechenko:
nntp://newsgroups.codegear.com/borland.public.delphi.oleautomation/65167
}
constructor TOleCollectionEnumFactory.Create(const ACollection:
IDispatch);
begin
inherited Create;
if not Supports(GetDispatchPropValue(ACollection, -4), IEnumVariant,
FEnum) then
raise EIntfCastError.Create('Interface does not support OLE
enumeration.');
end;
function TOleCollectionEnumFactory.GetEnumerator: TOleCollectionEnum;
begin
Result := TOleCollectionEnum.Create(FEnum);
end;
{ TOleCollectionEnum }
constructor TOleCollectionEnum.Create(const AEnum: IEnumVARIANT);
begin
inherited Create;
FEnum := AEnum;
end;
{ inspired by a newsgroup posting by Chris Cheney:
nntp://newsgroups.codegear.com/borland.public.delphi.oleautomation/65159
}
function TOleCollectionEnum.MoveNext: Boolean;
var
lFetched: Cardinal;
lResult: Cardinal;
begin
Result := False;
if FEnum <>nil then
begin
lFetched := 0;
lResult := FEnum.Next(1, OleVariant(FElem), lFetched);
case lResult of
S_OK: Result := (lFetched>0);
S_FALSE: ;
else
OleCheck(lResult);
end;
end;
end;
--
 

Re:Outlook (2003): looping through item selections > 250

Are you sure you wnat the selection? 3000 is quite a few items to select.
Processing the whole folder using MAPI (IMAPITable in particular) would
probably be a lot faster.
--
Dmitry Streblechenko (MVP)
www.dimastr.com/
OutlookSpy - Outlook, CDO
and MAPI Developer Tool
-
"Oliver Giesen" <XXXX@XXXXX.COM>writes
Quote
Dmitry Streblechenko writes:

>If _NewEnum is not explicitly exposed, it might still be available by
>callign IDispatch::Invoke() with dispId = -4. AFAIK that is what VB
>does, and "for each" for the Selection collection works fine in all
>versions of Outlook.

Great! This works even on the Selection (though it does not appear to
be noticeably faster than the regular for to loop).

I have also found that now that the iteration itself has become pretty
fast (<200ms for ~3000 items) the real bottleneck is accessing the
Selection property in the first place: For the ~3000 items mentioned
before this alone takes about 1600ms... and it doesn't seem to get
faster by using late binding. Yet, when I call the essentially
identical VBA macro (see previous posts) that penalty does not seem to
apply... i.e. I get the result in less than a second.


Anyway, I have now wrapped yours and Chris' suggestions about the Foreach
iteration into an enumerator so you can use for-in loops on such
collections in Delphi (see below). Feel free to comment and/or share.

Usage would be something like:

for lItem in EnumOleCollection(OutlookApp.ActiveExplorer.Selection) do
...


Here's the code:

type
TOleCollectionEnum = class
private
FElem: OleVariant;
FEnum: IEnumVariant;
public
constructor Create(const AEnum: IEnumVARIANT);
function MoveNext: Boolean;
property Current: OleVariant read FElem;
end;
IOleCollectionEnumFactory = interface
function GetEnumerator: TOleCollectionEnum;
end;
TOleCollectionEnumFactory = class(TInterfacedObject,
IOleCollectionEnumFactory)
private
FEnum: IEnumVariant;
public
constructor Create(const ACollection: IDispatch);
function GetEnumerator: TOleCollectionEnum;
end;

function EnumOleCollection(const ACollection: IDispatch):
IOleCollectionEnumFactory;
begin
Result := TOleCollectionEnumFactory.Create(ACollection);
end;

{ TOleCollectionEnumFactory }

{ inspired by a newsgroup posting by Dmitry Streblechenko:
nntp://newsgroups.codegear.com/borland.public.delphi.oleautomation/65167
}
constructor TOleCollectionEnumFactory.Create(const ACollection:
IDispatch);
begin
inherited Create;
if not Supports(GetDispatchPropValue(ACollection, -4), IEnumVariant,
FEnum) then
raise EIntfCastError.Create('Interface does not support OLE
enumeration.');
end;

function TOleCollectionEnumFactory.GetEnumerator: TOleCollectionEnum;
begin
Result := TOleCollectionEnum.Create(FEnum);
end;

{ TOleCollectionEnum }

constructor TOleCollectionEnum.Create(const AEnum: IEnumVARIANT);
begin
inherited Create;
FEnum := AEnum;
end;

{ inspired by a newsgroup posting by Chris Cheney:
nntp://newsgroups.codegear.com/borland.public.delphi.oleautomation/65159
}
function TOleCollectionEnum.MoveNext: Boolean;
var
lFetched: Cardinal;
lResult: Cardinal;
begin
Result := False;
if FEnum <>nil then
begin
lFetched := 0;
lResult := FEnum.Next(1, OleVariant(FElem), lFetched);
case lResult of
S_OK: Result := (lFetched>0);
S_FALSE: ;
else
OleCheck(lResult);
end;
end;
end;

--