Search code examples
delphims-wordoffice-interop

`EOleException: Call was rejected by callee` while iterating through `Office.Interop.Word.Documents`


I've been working with Word2010.pas for the past week and everything went well, until I found out that if you open a document manually, edit it (but don't save), press Alt+F4, a prompt will show up saying if you want to save your document or not, leave it like that. Go into code and try to access that document, all calls will result in EOleException: Call was rejected by callee. Once you cancel that Word save prompt, everything works fine.

I came across this while writing code that periodically checks if a document is open. Here is the function that checks if the document is open: (function runs in a timer every 2 seconds)

function IsWordDocumentOpen(FileName: string): Boolean;
var
  WordApp: TWordApplication;
  I: Integer;
begin
  Result := False;
  try
    WordApp := TWordApplication.Create(nil);
    try          
      WordApp.Connect;
      for I := 1 to WordApp.Documents.Count do
      begin
        try
          if WordApp.Documents.Item(I).FullName = FileName then
          begin
            Result := True;
            System.Break;
          end;
        except
          on E: EOleException do
            // I always end up here while the document has the prompt
        end;
      end;
    finally
      FreeAndNil(WordApp);
    end;
  finally
    //
  end;
end;

Does anyone have any experience with this? Is there some sort of a lock that I'm not aware of?

UPDATE #1: So far the only solution I could find was to implement IOleMessageFilter, this way I do not receive any exceptions but the program stops and waits on the line WordApp.Documents.Item(I).FullName, but that is not what I want. Implementation of IOleMessageFilter goes like this:

type
  IOleMessageFilter = class(TInterfacedObject, IMessageFilter)
  public
    function HandleInComingCall(dwCallType: Longint; htaskCaller: HTask;
      dwTickCount: Longint; lpInterfaceInfo: PInterfaceInfo): Longint;stdcall;
    function RetryRejectedCall(htaskCallee: HTask; dwTickCount: Longint;
      dwRejectType: Longint): Longint;stdcall;
    function MessagePending(htaskCallee: HTask; dwTickCount: Longint;
      dwPendingType: Longint): Longint;stdcall;
    procedure RegisterFilter();
    procedure RevokeFilter();
  end;

implementation

function IOleMessageFilter.HandleInComingCall(dwCallType: Integer; htaskCaller: HTask; dwTickCount: Integer; lpInterfaceInfo: PInterfaceInfo): Longint;
begin
  Result := 0;
end;

function IOleMessageFilter.MessagePending(htaskCallee: HTask; dwTickCount, dwPendingType: Integer): Longint;
begin
  Result := 2 //PENDINGMSG_WAITDEFPROCESS
end;

procedure IOleMessageFilter.RegisterFilter;
var
  OldFilter: IMessageFilter;
  NewFilter: IMessageFilter;
begin
  OldFilter := nil;
  NewFilter := IOleMessageFilter.Create;
  CoRegisterMessageFilter(NewFilter,OldFilter);
end;

function IOleMessageFilter.RetryRejectedCall(htaskCallee: HTask; dwTickCount, dwRejectType: Integer): Longint;
begin
  Result := -1;
  if dwRejectType = 2 then
    Result := 99;
end;

procedure IOleMessageFilter.RevokeFilter;
var
  OldFilter: IMessageFilter;
  NewFilter: IMessageFilter;
begin
  OldFilter := nil;
  NewFilter := nil;
  CoRegisterMessageFilter(NewFilter,OldFilter);
end;

end;

BEST SOLUTION SO FAR: I used IOleMessageFilter implementation like this: (remember this will stop and wait on the line where I previously got an exception)

function IsWordDocumentOpen(FileName: string): Boolean;
var
  OleMessageFilter: IOleMessageFilter;
  WordApp: TWordApplication;
  I: Integer;
begin
  Result := False;
  try
    OleMessageFilter := IOleMessageFilter.Create;
    OleMessageFilter.RegisterFilter;

    WordApp := TWordApplication.Create(nil);
    try
      WordApp.Connect;
      for I := 1 to WordApp.Documents.Count do
      begin
        if WordApp.Documents.Item(I).FullName = FileName then
        begin
          Result := True;
          System.Break;
        end;
      end;
    finally
      OleMessageFilter.RevokeFilter;
      FreeAndNil(WordApp);
      FreeAndNil(OleMessageFilter);
    end;
  finally
    //
  end;
end;

Solution

  • Actually, I think that the problem is simply that Word is busy doing a modal dialog and so can't respond to external COM calls. This trivial code produces the same error:

    procedure TForm1.Button1Click(Sender: TObject);
    begin
      Caption := MSWord.ActiveDocument.Name;
    end;
    

    Probably the simplest way to avoid this problem is to head it off before if happens. If you are using the TWordApplication server that comes with Delphi (on the Servers components tab), you can attach an event handler to its OnDocumentBeforeClose and use that to present your own "Save Y/N?" dialog and set the event's Cancel param to True to prevent Word's dialog from appearing.

    Update: If you try experimenting with this code while the Save dialog is popped up

    procedure TForm1.Button1Click(Sender: TObject);
    var
      vWin,
      vDoc,
      vApp : OleVariant;
    begin
      vWin := MSWord.ActiveWindow;
      Caption := vWin.Caption;
      vDoc := vWin.Document;
    
      vApp := vDoc.Application;  //  Attempt to read Word Document property
    
      Caption := vDoc.Path + '\';
      Caption := Caption + vDoc.Name;
    end;
    

    I think you'll find that any attempt to read from the vDoc object will result in the "Call was rejected ..." message, so I am beginning to think that this behaviour is by design - it's telling you that the object is not in a state that it can be interacted with.

    Interestingly, it is possible to read the Caption property of the vWin Window object, which will tell you the filename of the file but not the file's path.

    Realistically, I still think your best option is to try and get the OnDocumentBeforeClose event working. I don't have Word 2010 installed on this machine by Word 2007 works fine with the Word server objects derived from Word2000.Pas so you might try those instead of Word2010.Pas, just to see.

    Another possibility is simply to catch the "Call was rejected ..." exception, maybe return "Unavailable" as the document FullName, and try again later.

    If you're not using TWordApplication and don't know how to catch the OnDocumentBeforeClose for the method your using to access Word, let me know how you are accessing it and I'll see if I can dig out some code to do it.

    I vaguely recall there's a way of detecting that Word is busy with a modal dialog - I'll see if I can find where I saw that a bit later if you still need it. Your IOleMessageFilter looks more promising than anything I've found as yet, though.