Daily D
 

Word 2003 - neotevírají se dokumenty

Úvod - Irsko - Hudba - Vše ostatní - Portfólio - Kamarádi - Kecárna - Vizitka


Nedávno jsem v práci narazil na zajímavý problém s MS Wordem 2003. Najednou z ničeho nic se přestaly otevírat téměř všechny firemní dokumenty, při pokusu o jejich otevření prostě Word zatuhnul. Zajímavé bylo, že problém se nevyskytoval na PC z MS Office 2007. Toho jsem ale využil při pátrání po příčině problému. Nejprve jsme zjistili, že pouhé pře uložení v MS Word 2007 do nového formátu a zpět nepomohlo. Při dalším pátrání po příčině problému jsem využil toho, že formát .docx není vlastně nic jiného, než přejmenovaný .zip soubor z hromadou .xml souborů uvnitř čehož jsem využil při pátraní ve „vnitřnostech“ poškozeného dokumentu. Postupně jsem tyto dokumenty projížděl a snažil se najít něco „neobvyklého“, což se mi strašně dlouho nedařilo. Pak kolega zjistil, že pomůže celý dokument zkopírovat do schránky a vložit do nového dokumentu. Tohoto jsem využil a dal jsem vedle sebe dokument, kde se chyba vyskytovala a dokument opravený a opět jsem u nich chtěl projel jednotlivé .xml soubory. Ale už ve chvíli, kdy jsem porovnával obsahy jednotlivých adresářů jsem narazil na to, že v opravené verzi chybí v adresáři word\_rels soubor settings.xml.rels. když jsem jej otevřel, tak se přede mnou objevil následující obsah:
 
 <?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/attachedTemplate" Target="file:///\\Srvr01\projekty\_Grafický_manuál\Sablony\Dokument%20AD.dot" TargetMode="External" />
 </Relationships>

 
Hned mi bylo jasné, odkud „vítr fouká“. V podstatě se jedná o to, že pokud vytvořím nový dokument ze šablony umístěné na serveru, tak si Word zapamatuje, kde se šablona nacházela a při každém otevření se ji snaží z nějakého důvodu hledat. Do určitého okamžiku bylo vše v pořádku, jenže pak nastal čas obměny strojového parku a tím pádem i odstávka starého serveru. A to byl kámen úrazu, Word se pokouší kontaktovat neexistující počítač a logicky zamrzne. Mezitím jsem ještě zjistil, že pokud se Word 2003 nechá dostatečně dlouho otevírat (tak 5 – 10 minut), tak se dokument otevře, což přesně korespondovalo se zjištěným problémem. Ihned jsem v takto otevřeném dokumentu ve Wordu 2003 zamířil do nabídky Nástroje – Šablony a doplňky (ve Wordu 2007 je to cesta: možnosti aplikace Word – Doplňky – Spravovat, v seznamu vybrat Doplňky aplikace Word a stisknout vybrat), kde byla dle očekávání výše uvedená cesta. Stačilo ji smazat a bylo po problému.
Jenže jak tento problém vyřešit systémově, když máte ve firmě řádově 100 000 takto postižených dokumentu?

Řešení problému

Nejprve jsem si nahrál makro, které tento problém opraví v konkrétním dokumentu:
 
 With ActiveDocument
        .AttachedTemplate = ""
 End With

 
Pak jsem zapátral na internetu po tom jak tento příkaz zavolat z .net na libovolném dokumentu. Narazil jsem na tyto stránky: http://www.codeguru.com/columns/experts/article.php/c9733, ze kterých jsem vyšel. Řešení je následující:
 
    Public Sub Repair(ByVal mDocument As String)
        Dim app As Microsoft.Office.Interop.Word.Application
        Try
            app = New Microsoft.Office.Interop.Word.Application()
            Dim Missing As Object = System.Reflection.Missing.Value
            Dim doc As Microsoft.Office.Interop.Word.Document
 
            doc = app.Documents.Open(mDocument, Missing, Missing, Missing, Missing, Missing, Missing, Missing, Missing, Missing, Missing, Missing, Missing, Missing, Missing, Missing)
 
            ' Odebere cestu ke vzorovému deokumentu
            doc.AttachedTemplate = ""
 
            doc.Save()
            doc.Close(Missing, Missing, Missing)
 
            mFinish(mDocument)
        Catch ex As Exception
            ' Obsluha případné chyby:
            mCancel(mDocument, ex.Message)
        End Try
        Try
            app.Quit()
        Catch ex As Exception
            ' Obsluha případné chyby:
            mCancel(mDocument, ex.Message)
        End Try
    End Sub

 
Kód je potřeba spouštět přímo na PC z nainstalovaným Office 2007, protože tam se problém nevyskytuje a tím pádem není problém na nich opravu provádět. Tento kód jsem doplnil o jednoduché procházení adresářů:
 
    Private Sub LoadDirectories(ByVal Path As String)
        Dim f() As IO.FileInfo
        f = New IO.DirectoryInfo(Path).GetFiles("*.doc")
 
        For Each f1 As IO.FileInfo In f
            Repair(f1.FullName)
        Next
 
        Dim d() As IO.DirectoryInfo
        d = New IO.DirectoryInfo(Path).GetDirectories()
 
        For Each d1 As IO.DirectoryInfo In d
            LoadDirectories(d1.FullName)
        Next
    End Sub

 
Problém tohoto řešení spočívá v tom, že je mimořádně pomalé. Vždy se čeká na otevření Wordu a dokumentu v něm, pak se musí provést oprava, uložit jej a zavřít. Proto je potřeba kód doplnit o podporu více-vláknového zpracování. V podstatě jde o to, že se vedle sebe paralelně spouští několik vláken. Každé vlákno si otevírá svoji vlastní instanci Wordu, ve které provede požadované operace a zase ji zavře. Nesmí se ale otevírat okamžitě za sebou, ale je vždy potřeba chviličku počkat, protože jinak se bude vůči naši aplikaci tvářit jako „zakousnutý“ a program skončí chybou. Další věcí, kterou je potřeba ošetřit je, že se nesmí otevírat příliš mnoho vláken naráz - vzhledem k tomu, že každé vlákno si otevírá svoji vlastní instanci Wordu, tak by to bylo mimořádně náročné jak na paměť a na procesor, takže ve výsledku by došlo naopak k celkovému zpomalení, možná až zakousnutí počítače. Proto je nutné hlídat počet aktivních vláken, a pokud dosáhnou určitého počtu, tak dočasně zabránit otevírání dalších (40 instancí Wordu se dá na průměrně výkonném PC zvládnout).
 

více-vláknové zpracování

Pro podporu více-vláknového zpracování je potřeba z procedury Repair  odstranit parametry (při volání nového vlákna se nedají předávat parametry) a obalit ji pomoci nové třídy do „obálky“, přes kterou této proceduře budeme předávat parametry
 
Public Class Clean
    Public Delegate Sub Canceled(ByVal FileName As String, ByVal mError As String)
    Public Delegate Sub Finished(ByVal FileName As String)
 
    Private mDocument As String
    Private mCancel As Canceled
    Private mFinish As Finished
 
    Public Sub New(ByVal DocumentPath As String, ByVal can As Canceled, ByVal fin As Finished)
        mDocument = DocumentPath
        mCancel = can
        mFinish = fin
    End Sub
 
    Public Sub Repair()
 
        ......
 
    End Sub
End Class

 
Pomoci delegátů se předávají hlavnímu vláknu informace o tom, že vlákno dokončilo svoji činnost a zda úspěšně, nebo neúspěšně.
 
Dále je potřeba v proceduře obsluhující procházení adresářů doplnit cyklus procházející soubory v adresářích o tento kód:
 
    Private Count As Integer = 0
    Private MaxCout As Integer = 40
    Private mStop As Boolean = False
    Private SleepTime As Integer = 800
 
        ......
 
        For Each f1 As IO.FileInfo In f
            ' V cyklu čekám na uvolnění vlákna
            While mCount = MaxCout
                Threading.Thread.Sleep(100)
                System.Windows.Forms.Application.DoEvents()
            End While
            System.Windows.Forms.Application.DoEvents()
            If mStop Then Exit Sub
            If Not Microsoft.VisualBasic.Left(f1.Name, 2) = "~$" Then
                Dim mClean As New Clean(f1.FullName, AddressOf Cancel, AddressOf Finish)
                Dim thr As New Threading.Thread(AddressOf mClean.Repair)
                thr.Start()
                WriteLog("Spouštím: " & f1.FullName)
                System.Windows.Forms.Application.DoEvents()
                Count += 1
                Threading.Thread.Sleep(SleepTime)
            End If
        Next
        If mStop Then Exit Sub
        ......

 
 
Kód ošetřuje, aby nebyly otevírány dočasné dokumenty Wordu (začínající písmeny ~$), vytvoří novou proměnnou pomoci námi vytvořené třídy Clean, které předá potřebné parametry (cestu k souboru a odkazy pro delegáty), vytvoří a spustí nové vlákno. Zároveň si hlídám počet již otevřených vláken (pomoci proměnné mCount) a pokud jich je otevřených příliš mnoho, tak čekám, že nějaké vlákno ukončí svoji činnost. Jak jsem již uvedl výše, při ukončení činnosti vlákna je vždy volán delegát. Pro obsluhu delegátů je použit následující kód:
 
    Private Sub Cancel(ByVal FileName As String, ByVal mError As String)
        Count -= 1
        WriteLog("Chyba: " & FileName & vbNewLine & mError & vbNewLine)
        System.Windows.Forms.Application.DoEvents()
    End Sub
 
    Private Sub Finish(ByVal FileName As String)
        Count -= 1
        WriteLog("Dokončeno: " & FileName)
        PocetDokumentu()
        System.Windows.Forms.Application.DoEvents()
    End Sub

 

Logování

Protože chci jako uživatel vidět, které dokumenty se aktuálně zpracovávají a s jakými výsledky a taky kolik dokumentů již bylo „opraveno“, tak na formůláž přidám dvě textové pole (tbPocetDokumentu  a LogBox). Problém je v tom, že do formulářových prvků nelze zapisovat z jiného, než hlavního vlákna. U zápisu počtu „opravených“ dokumentů je řešení jednoduché, protože stačí inkrementovat jednu proměnnou a tu vypsat:
 
    Private Pocet As Integer = 0
    Public Sub PocetDokumentu()
        If Me.tbPocetDokumentu.InvokeRequired Then
            Dim d As New SetTextCallback(AddressOf PocetDokumentu)
            Me.tbPocetDokumentu.Invoke(d, New Object() {})
        Else
            Pocet += 1
            tbPocetDokumentu.Text = Pocet
        End If
    End Sub

 
Horší je to u zapisování do „logu“. Tam je již potřeba předávat parametr s vypisovaným textem. Toto lze nejjednodušeji dosáhnout opět přes vlastní třídu, které při vytváření daný parametr předám. Problém je v tom, že tato třída „nevidí“ na formulářové prvky, proto ji přes ByRef předám odkaz na proměnnou reprezentující formulářový prvek, s kterou pak již můžu pracovat jako bych pracoval přímo s daným prvkem:
 
Public Class cLogWrite
    Delegate Sub SetTextCallback()
 
    Private LogBox As System.Windows.Forms.TextBox
    Private mLogText As String
 
    Public Sub New(ByRef Log As System.Windows.Forms.TextBox, ByVal LogText As String)
        Me.LogBox = Log
        Me.mLogText = LogText
        Write()
    End Sub
 
    Private Sub Write()
        If Me.LogBox.InvokeRequired Then
            Dim d As New SetTextCallback(AddressOf Write)
            Me.LogBox.Invoke(d, New Object() {})
        Else
            Me.LogBox.Text = Me.LogBox.Text & Me.mLogText & vbNewLine
            Me.LogBox.Select(Me.LogBox.Text.Length, Me.LogBox.Text.Length)
            Me.LogBox.ScrollToCaret()
        End If
    End Sub
End Class

 
Pří zápisu do „logu“ pak již jen volám tuto třídu:
 
    Private Sub WriteLog(ByVal Message As String)
        Dim Log As New cLogWrite(Me.LogBox, Message)
    End Sub

 

Ukončení všech vláken

Poslední věcí je jak po ukončení procházení adresářů rozpoznat, že všechny další vlákna ukončili svoji činnost. Toho nejjednodušeji dosáhneme tím, že proměnou Count nahradíme vlastností, ve které si toto již ošetříme:
 
    Private mCount As Integer = 0
    Public Property Count() As Integer
        Get
            Return mCount
        End Get
        Set(ByVal value As Integer)
            mCount = value
            WriteCount()
            System.Windows.Forms.Application.DoEvents()
            'Zkončilo procházení adresářů a počítadlo vláken se dostalu na nulu
            If Count = 0 And mStop Then
                MsgBox("Oprava dokončena", MsgBoxStyle.Information, "Hotovo")
            End If
        End Set
    End Property

 
To je tak v kostce zhruba vše. Ještě zbývá dořešit tlačítka pro výběr adresáře, v kterém chci provést opravu, tlačítka pro spuštění, nebo případné zastavení programu. Dále se dá formulář doplnit o prvky, ve kterých si nadefinuji maximální počet současně spuštěných vláken, popřípadě čekací dobu po spuštění nového vlákna.



Komentáře:

Možnosti: Nový vzkaz

Autor: jura 26.8.2009 10:30:44
Nazev: ad dotaz
Jak jsem si to přečetl ještě jednou tak by my to asi stejně nepomohlo neb mám jen office 2003   

Jura

Odpovědět
 
Autor: Jura 26.8.2009 10:07:56
Nazev: dotaz
Velmi zajmavý článek ohledně dlouhého oteviraní dokumentů. Jelikož se ve VBA neorientuji, lze kod poslat na email v celku ? Lze to řešit i u excelu ? A zajmalo by mně, jak se dá zamezit opětovné chybě když dojde k další výměně PC kde jsou uloženy šablony ?

Děkuji za tvůj čas i odpověď

Jurka.k@email.cz

jura

Odpovědět
 

<< - >>


Copyright © B&B Team 2007 Daily D