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 |
| |
|
<< - >> |
|