OpenXML IsolatedStorage threading - vb.net

According to this article OpenXML is not thread safe when it's "MemoryStreams reach the high water mark" and has to switch to IsolatedStorage.
This occurs on workbooks smaller than even 1mb because the UNCOMPRESSED data is likely 10x that size.
I really need to create xlsx files concurrently, in particular, abnormally large ones with concurrency. The MS solution is to implement something like the following(converted from C#), but I'm not sure what to do with this.
Public Class PackagePartStream
Private _stream As Stream
Private Shared _m As New Mutex(False)
Public Sub New(ByVal Stream As Stream)
_stream = Stream
End Sub
Public Function Read(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer) As Integer
Return _stream.Read(buffer, offset, count)
End Function
Public Sub Write(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer)
_m.WaitOne(Timeout.Infinite, False)
_stream.Write(buffer, offset, count)
_m.ReleaseMutex()
End Sub
Public Sub Flush()
_m.WaitOne(Timeout.Infinite, False)
_stream.Flush()
_m.ReleaseMutex()
End Sub
End Class
My best guess so far would be something like this but I have a feeling I'm over simplifying this and the mutex needs to be closer to the function handling OpenXML's WriteElement
Dim stream As New PackagePartStream()
Using document As SpreadsheetDocument = SpreadsheetDocument.Create(stream, SpreadsheetDocumentType.Workbook, True)
WriteExcelFile(ds, document)
End Using
I haven't done much threading in .NET but hopefully i can be pointed in the right direction by someone.

I hate to answer my own question but don't use the OpenXML SDK if you're building dynamic reports on a webserver. Use EPplus instead. It uses an alternative packaging library that does not use IsolagedStorage. It's a shame Microsoft hasn't addressed this issue.

Related

Window explorer is freezing when open

I need to open the specific folder for a file and I am doing it with:
file = Directory.GetFiles(filepath,Filename,
SearchOption.AllDirectories).FirstOrDefault()
Process.Start("explorer.exe", "/select," & file.ToString)
This code is immediately opening the folder which is already fully loaded, but it doesnt seem enabled, endeed I cant do any action in it. The form is not freezing.
Thanks
I'll give you an answer in two parts...
Firstly, if the GetFiles() call takes to long and freezes the form (which doesn't seem to be the current problem), you should do the following:
Use EnumerateFiles() instead because in this case, FirstOrDefault() will return immediately after finding a matching file, unlike GetFiles() which will get all the files first before calling FirstOrDefault().
Wrap the call to EnumerateFiles() in a Task.Run() to execute it on a worker thread in case the search takes a little too long:
' Or:
' Private Async Sub SomeEventHandler()
Private Async Function ParentMethod() As Task
Dim filePath As String = Await Task.Run(
Function()
Return Directory.EnumerateFiles(dirPath, FileName, SearchOption.AllDirectories).
FirstOrDefault()
End Function)
' TODO: Use `filePath` to open the folder and select the file.
End Function
Secondly, do not use Process.Start("explorer.exe", "/select") because a) it starts a new Explorer.exe process rather than opening the directory in the current one, b) it seems to be causing you some issues, and c) it has some limitations.
Instead, use the approach demonstrated in the answer linked in point (c) above. The code is in C# but it can be easily converted to VB. Here's the VB version of the code (with an additional overload).
Add the following class to your project:
Imports System.IO
Imports System.Runtime.InteropServices
Public Class NativeMethods
<DllImport("shell32.dll", SetLastError:=True)>
Private Shared Function SHOpenFolderAndSelectItems(
pidlFolder As IntPtr, cidl As UInteger,
<[In], MarshalAs(UnmanagedType.LPArray)> apidl As IntPtr(),
dwFlags As UInteger) As Integer
End Function
<DllImport("shell32.dll", SetLastError:=True)>
Private Shared Sub SHParseDisplayName(
<MarshalAs(UnmanagedType.LPWStr)> name As String,
bindingContext As IntPtr, <Out> ByRef pidl As IntPtr,
sfgaoIn As UInteger, <Out> ByRef psfgaoOut As UInteger)
End Sub
Public Shared Sub OpenFolderAndSelectFile(filePath As String)
Dim dirPath As String = Path.GetDirectoryName(filePath)
Dim fileName As String = Path.GetFileName(filePath)
OpenFolderAndSelectFile(dirPath, fileName)
End Sub
Public Shared Sub OpenFolderAndSelectFile(dirPath As String, fileName As String)
Dim nativeFolder As IntPtr
Dim psfgaoOut As UInteger
SHParseDisplayName(dirPath, IntPtr.Zero, nativeFolder, 0, psfgaoOut)
If nativeFolder = IntPtr.Zero Then
' Log error, can't find folder
Return
End If
Dim nativeFile As IntPtr
SHParseDisplayName(Path.Combine(dirPath, fileName),
IntPtr.Zero, nativeFile, 0, psfgaoOut)
Dim fileArray As IntPtr()
If nativeFile = IntPtr.Zero Then
' Open the folder without the file selected if we can't find the file
fileArray = New IntPtr(-1) {}
Else
fileArray = New IntPtr() {nativeFile}
End If
SHOpenFolderAndSelectItems(nativeFolder, CUInt(fileArray.Length), fileArray, 0)
Marshal.FreeCoTaskMem(nativeFolder)
If nativeFile <> IntPtr.Zero Then
Marshal.FreeCoTaskMem(nativeFile)
End If
End Sub
End Class
Then, you can easily call it like this:
NativeMethods.OpenFolderAndSelectFile(filePath)
Some additional notes:
You should choose meaningful variable names. filePath should refer to the path of a file. If you want to refer to a folder/directory path, use something like dirPath or folderPath instead.
You don't need to call .ToString() on a variable that's already of a String type.
I would change the variable name file to something else. Maybe foundFile. After all File is the name of a class in System.IO and vb.net is not case sensitive. Your code works fine for me with the variable name change. Also got rid of the .ToString. I used .EnumerateFiles as commented by #jmcilhinney in the question you deleted. I purposely chose a path with all sorts of strange characters and it still worked.
Private Sub OPCode()
Dim filepath = "C:\Users\xxxx\Documents\TextNotes\Dogs & Cats (Pets)"
Dim Filename = "Specialty Vets.txt"
Dim foundFile = Directory.EnumerateFiles(filepath, Filename,
IO.SearchOption.AllDirectories).FirstOrDefault()
Process.Start("explorer.exe", "/select," & foundFile)
End Sub

Showing progress of ZipFiles Class

I was wondering, how can I get the percentage of this being done, so I can display it on a progress bar?
ZipFile.CreateFromDirectory("C:\temp\folder", "C:\temp\folder.zip")
and also
ZipFile.ExtractToDirectory("C:\temp\folder.zip", "C:\temp\folder")
This doesnt have any events or callbacks that you can use to report progress. Simply means you cant with the .Net version. If you used the 7-Zip library you can do this easily.
I came across this question while checking for related questions for the identical question, asked for C# code. It is true that the .NET static ZipFile class does not offer progress reporting. However, it is not hard to do using the ZipArchive implementation, available since earlier versions of .NET.
The key is to use a Stream wrapper that will report bytes read and written, and insert that in the data pipeline while creating or extracting the archive.
I wrote a version in C# for an answer to the other question, and since I didn't find any VB.NET examples, figured it would be helpful to include a VB.NET version on this question.
(Arguably, I could include both examples in a single answer and propose closing one of the questions as a duplicate of the other. But since it's doubtful the close vote would result in an actual closure, the connection between the two questions would not be as obvious as it should be. I think for best visibility to future users trying to find the solution appropriate for their needs, leaving this as two different questions is better.)
The foundation of the solution is the Stream wrapper class:
StreamWithProgress.vb
Imports System.IO
Public Class StreamWithProgress
Inherits Stream
' NOTE For illustration purposes. For production code, one would want To
' override *all* of the virtual methods, delegating to the base _stream object,
' to ensure performance optimizations in the base _stream object aren't
' bypassed.
Private ReadOnly _stream As Stream
Private ReadOnly _readProgress As IProgress(Of Integer)
Private ReadOnly _writeProgress As IProgress(Of Integer)
Public Sub New(Stream As Stream, readProgress As IProgress(Of Integer), writeProgress As IProgress(Of Integer))
_stream = Stream
_readProgress = readProgress
_writeProgress = writeProgress
End Sub
Public Overrides ReadOnly Property CanRead As Boolean
Get
Return _stream.CanRead
End Get
End Property
Public Overrides ReadOnly Property CanSeek As Boolean
Get
Return _stream.CanSeek
End Get
End Property
Public Overrides ReadOnly Property CanWrite As Boolean
Get
Return _stream.CanWrite
End Get
End Property
Public Overrides ReadOnly Property Length As Long
Get
Return _stream.Length
End Get
End Property
Public Overrides Property Position As Long
Get
Return _stream.Position
End Get
Set(value As Long)
_stream.Position = value
End Set
End Property
Public Overrides Sub Flush()
_stream.Flush()
End Sub
Public Overrides Sub SetLength(value As Long)
_stream.SetLength(value)
End Sub
Public Overrides Function Seek(offset As Long, origin As SeekOrigin) As Long
Return _stream.Seek(offset, origin)
End Function
Public Overrides Sub Write(buffer() As Byte, offset As Integer, count As Integer)
_stream.Write(buffer, offset, count)
_writeProgress?.Report(count)
End Sub
Public Overrides Function Read(buffer() As Byte, offset As Integer, count As Integer) As Integer
Dim bytesRead As Integer = _stream.Read(buffer, offset, count)
_readProgress?.Report(bytesRead)
Return bytesRead
End Function
End Class
The wrapper class can be used to implement progress-aware versions of the ZipFile static methods:
ZipFileWithProgress.vb
Imports System.IO
Imports System.IO.Compression
NotInheritable Class ZipFileWithProgress
Private Sub New()
End Sub
Public Shared Sub CreateFromDirectory(
sourceDirectoryName As String,
destinationArchiveFileName As String,
progress As IProgress(Of Double))
sourceDirectoryName = Path.GetFullPath(sourceDirectoryName)
Dim sourceFiles As FileInfo() = New DirectoryInfo(sourceDirectoryName).GetFiles("*", SearchOption.AllDirectories)
Dim totalBytes As Double = sourceFiles.Sum(Function(f) f.Length)
Dim currentBytes As Long = 0
Using archive As ZipArchive = ZipFile.Open(destinationArchiveFileName, ZipArchiveMode.Create)
For Each fileInfo As FileInfo In sourceFiles
' NOTE: naive method To Get Sub-path from file name, relative to
' input directory. Production code should be more robust than this.
' Either use Path class Or similar to parse directory separators And
' reconstruct output file name, Or change this entire method to be
' recursive so that it can follow the sub-directories And include them
' in the entry name as they are processed.
Dim entryName As String = fileInfo.FullName.Substring(sourceDirectoryName.Length + 1)
Dim entry As ZipArchiveEntry = archive.CreateEntry(entryName)
entry.LastWriteTime = fileInfo.LastWriteTime
Using inputStream As Stream = File.OpenRead(fileInfo.FullName)
Using outputStream As Stream = entry.Open()
Dim progressStream As Stream = New StreamWithProgress(inputStream,
New BasicProgress(Of Integer)(
Sub(i)
currentBytes += i
progress.Report(currentBytes / totalBytes)
End Sub), Nothing)
progressStream.CopyTo(outputStream)
End Using
End Using
Next
End Using
End Sub
Public Shared Sub ExtractToDirectory(
sourceArchiveFileName As String,
destinationDirectoryName As String,
progress As IProgress(Of Double))
Using archive As ZipArchive = ZipFile.OpenRead(sourceArchiveFileName)
Dim totalBytes As Double = archive.Entries.Sum(Function(e) e.Length)
Dim currentBytes As Long = 0
For Each entry As ZipArchiveEntry In archive.Entries
Dim fileName As String = Path.Combine(destinationDirectoryName, entry.FullName)
Directory.CreateDirectory(Path.GetDirectoryName(fileName))
Using inputStream As Stream = entry.Open()
Using outputStream As Stream = File.OpenWrite(fileName)
Dim progressStream As Stream = New StreamWithProgress(outputStream, Nothing,
New BasicProgress(Of Integer)(
Sub(i)
currentBytes += i
progress.Report(currentBytes / totalBytes)
End Sub))
inputStream.CopyTo(progressStream)
End Using
End Using
File.SetLastWriteTime(fileName, entry.LastWriteTime.LocalDateTime)
Next
End Using
End Sub
End Class
The .NET built-in implementation of IProgress(Of T) is intended for use in contexts where there is a UI thread where progress reporting events should be raised. As such, when used in a console program, like which I used to test this code, it will default to using the thread pool to raise the events, allowing for the possibility of out-of-order reports. To address this, the above uses a simpler implementation of IProgress(Of T), one that simply invokes the handler directly and synchronously.
BasicProgress.vb
Class BasicProgress(Of T)
Implements IProgress(Of T)
Private ReadOnly _handler As Action(Of T)
Public Sub New(handler As Action(Of T))
_handler = handler
End Sub
Private Sub Report(value As T) Implements IProgress(Of T).Report
_handler(value)
End Sub
End Class
And naturally, it's useful to have an example with which to test and demonstrate the code.
Module1.vb
Imports System.IO
Module Module1
Sub Main(args As String())
Dim sourceDirectory As String = args(0),
archive As String = args(1),
archiveDirectory As String = Path.GetDirectoryName(Path.GetFullPath(archive)),
unpackDirectoryName As String = Guid.NewGuid().ToString()
File.Delete(archive)
ZipFileWithProgress.CreateFromDirectory(sourceDirectory, archive,
New BasicProgress(Of Double)(
Sub(p)
Console.WriteLine($"{p:P2} archiving complete")
End Sub))
ZipFileWithProgress.ExtractToDirectory(archive, unpackDirectoryName,
New BasicProgress(Of Double)(
Sub(p)
Console.WriteLine($"{p:P0} extracting complete")
End Sub))
End Sub
End Module
Additional notes regarding this implementation can be found in my answer to the related question.

SendMessage between WinForms Applications - form needs focus

I'm using the Windows Messages API to communicate between two Windows Forms Apps. I took out a form that was a part of the application and made it into it's own app so that when it is loading the user can still work on the other forms now in a separate app from the one form.
I need to be able to communicate between the two apps so that the now separate app form can tell the main app what to open.
I use this code on the main form of the main app and it works great... Except when the main form doesn't have focus.
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
Try
Select Case m.Msg
Case &H400
BS.BuildString(m.LParam)
Case Else
MyBase.WndProc(m)
End Select
Catch ex As Exception
End Try
End Sub
I've never used the Windows Message API before, I just grabbed this from looking up how to communicate between forms, so I'm new at this. What I do know from doing some more research is that I need a Message only Window to handle the messages. I don't understand how to do this.
I've looked at a few articles and solutions like this one and I think the problem I'm having is that I don't know how to implement it.
Edit 1
Here is how the second app finds and sends to the main app.
'used to send a message using SendMessage API to the
'main app to open the ID
Private WithEvents BS As New BuildString
'get this running process
Dim proc As Process = Process.GetCurrentProcess()
'get all other (possible) running instances
Dim processes As Process() = Process.GetProcessesByName("ProcessName")
If processes.Length > 0 Then
'iterate through all running target applications
For Each p As Process In processes
'now send the ID to the running instance of the main app
BS.PostString(p.MainWindowHandle, &H400, 0, "ID:" & ID)
Next
Else
MessageBox.Show("Main application not running")
End If
Here's the class for the BuildString that is in both apps.
Imports System.Text
Public Class BuildString
Private Declare Function PostMessage Lib "user32.dll" Alias "PostMessageA" (ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Integer
Public Event StringOK(ByVal Result As String)
Private hwnd As Integer = 0
Private wMsg As Integer = 0
Private wParam As Integer = 0
Private lParam As String = ""
Private tempA(-1) As Byte
Private enc As Encoding = Encoding.UTF8
Public Property Encode() As Encoding
Get
Return enc
End Get
Set(ByVal value As Encoding)
enc = value
End Set
End Property
Public Sub BuildString(ByVal b As IntPtr)
If b <> 0 Then
'build temp array
Dim tempB(tempA.Length) As Byte
tempA.CopyTo(tempB, 0)
tempB(tempA.Length) = b
ReDim tempA(tempB.Length - 1)
tempB.CopyTo(tempA, 0)
Else
'decode byte array to string
Dim s As String
If enc Is Encoding.UTF8 Then
s = Encoding.UTF8.GetString(tempA)
ElseIf enc Is Encoding.Unicode Then
s = Encoding.Unicode.GetString(tempA)
ElseIf enc Is Encoding.ASCII Then
s = Encoding.ASCII.GetString(tempA)
Else
s = Encoding.Default.GetString(tempA)
End If
'send out result string via event
RaiseEvent StringOK(s)
ReDim tempA(-1)
End If
End Sub
Public Sub PostString(ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer, ByVal lParam As String)
Me.hwnd = hwnd
Me.wMsg = wMsg
Me.wParam = wParam
Me.lParam = lParam
'create a new thread to post window message
Dim t As Threading.Thread
t = New Threading.Thread(AddressOf SendString)
t.Start()
End Sub
Private Sub SendString()
'create byte array
Dim ba() As Byte
'encode string to byte array
If enc Is Encoding.UTF8 Then
ba = Encoding.UTF8.GetBytes(lParam)
ElseIf enc Is Encoding.Unicode Then
ba = Encoding.Unicode.GetBytes(lParam)
ElseIf enc Is Encoding.ASCII Then
ba = Encoding.ASCII.GetBytes(lParam)
Else
ba = Encoding.Default.GetBytes(lParam)
End If
Dim i As Integer
For i = 0 To ba.Length - 1
'start post message
PostMessage(hwnd, wMsg, wParam, ba(i))
Next
'post a terminator message to destination window
PostMessage(hwnd, wMsg, wParam, 0)
End Sub
End Class
Here is the code on the main app that is run once the message gets decoded.
Private Sub SB_StringOK(ByVal Result As String) Handles BS.StringOK
Dim sArray() As String = Result.Split(";")
'rest of the code to open the ID
End Sub
Thanks to #JustinRyan I figured it out using FindWindow.
I added this to my second application that send the message.
Private Declare Function FindWindow1 Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Integer
Instead of finding the application process I find the window I want and send it directly to it in the send message.
Dim parenthwnd As Integer = FindWindow1(Nothing, "Form Name")
BS.PostString(parenthwnd, &H400, 0, "ID:" & ID)
It works just as I needed.
I'm not sure of how else to show the correct answer since the answer was in the comments.
Please let me know of the proper etiquette in this situation to give the user his due credit.
Instead of using this API, you could try using a new thread and having your second form created on this second thread, so both forms run on separate threads and can still be part of the same project
Dim Form2Thread As New System.Threading.Thread(Address of MethodName)
Sub Form1Load() Handles Form1.Shown
Form2Thread.Start()
End Sub
Sub MethodName()
'All your form creation code here
'Form2.Show()
End Sub
This is simple enough, but the downside is you can not directly edit a control or property of Form2 from a method running on your original thread. All changes to Form2 have to be made through your second thread.
(There is an exception to this but things start to get more complicated, search for how to do cross-thread operations)
The other solution is to use a Background Worker component. Plenty of tutorials around on using those.
[To add a bit more information:]
According to Process.MainWindowHandle,
The main window is the window opened by the process that currently has
the focus (the TopLevel form). You must use the Refresh method to
refresh the Process object to get the current main window handle if it
has changed.
TopLevel is defined here (as linked from MainWindowHandle) as,
A top-level form is a window that has no parent form, or whose parent
form is the desktop window. Top-level windows are typically used as
the main form in an application.
This would explain why messages are being sent elsewhere when the form is inactive. So, while using the Process properties may work for getting the window handle of single form applications, this makes it unreliable otherwise.
Coincidentally, FindWindow also states,
Retrieves a handle to the top-level window whose class name and window
name match the specified strings. This function does not search child
windows.
However, FindWindow (and FindWindowEx) is agnostic to focus (activation) and will therefore return a specific window's handle without concern of whether or not it is on top.

record amount of time computer has been in use?

i would like to have program a timer that will count the seconds during which there is mouse movement or any keyboard movement.
the point of this application is to record the amount of time an employee has been using the computer (does not matter what purpose or application it has been in use for)
i would like to do this in vb.net for winforms
I do exactly this using P/Invoke to talk to the GetLastInputInfo API.
Edit: Here's a complete VB.Net program to display the number of milliseconds since the last input event, system-wide. It sleeps for a second before getting the information, so it reports a time of around a thousand milliseconds, assuming you use the mouse or keyboard to run it. :-)
Imports System.Runtime.InteropServices
Module Module1
<StructLayout(LayoutKind.Sequential)> _
Public Structure LASTINPUTINFO
Public Shared ReadOnly SizeOf As Integer = Marshal.SizeOf(GetType(LASTINPUTINFO))
<MarshalAs(UnmanagedType.U4)> _
Public cbSize As Integer
<MarshalAs(UnmanagedType.U4)> _
Public dwTime As Integer
End Structure
<DllImport("user32.dll")> _
Public Function GetLastInputInfo(ByRef plii As LASTINPUTINFO) As Boolean
End Function
Sub Main()
Dim lii As New LASTINPUTINFO()
lii.cbSize = LASTINPUTINFO.SizeOf
lii.dwTime = 0
System.Threading.Thread.Sleep(1000)
GetLastInputInfo(lii)
MsgBox((Environment.TickCount - lii.dwTime).ToString)
End Sub
End Module

callback function from unmanaged dll in VB .NET

I'm trying to use an unmanaged dll in VB.NET. The example source code provided with the dll is in VB6 and below is my attempt to convert it to .NET. When the dll tries to do a callback I get a "Attempted to read or write protected memory" exception. I really don't care about the callback function getting actually called.
My code:
<DllImport("AlertMan.dll")> _
Public Shared Function AlertManC( _
ByVal CallbackAddr As AlertManCallbackDel) As Long
End Function
Public Delegate Sub AlertManCallbackDel(ByVal data As Long)
Public Sub AlertManCallback(ByVal data As Long)
End Sub
Public mydel As New AlertManCallbackDel(AddressOf AlertManCallback)
'protected memeory exception here
Dim IStat as Long = AlertManC(mydel)
Original VB6 example code:
Declare Function AlertManC _
Lib "AlertMan.dll" _
Alias "AlertManC" (ByVal CallbackAddr As Long) As Long
Private Sub AlertManCallback(ByVal data As Long)
End Sub
' calling code
Dim IStat As Long
IStat = AlertManC(AddressOf AlertManCallBack)
Original dll header
typedef void TACBFUNC(char *);
int AlertManC(TACBFUNC *WriteCaller cHANDLEPARM);
Can you post the original native definiton for AlertManC?
My guess though is that the data parameter of the callback function is actually an Integer vs. a Long. In VB6 I believe Long's were actually only 32 bits vs. VB.Net where they are 64 bit. Try this
<DllImport("AlertMan.dll")> _
Public Shared Function AlertManC(ByVal CallbackAddr As AlertManCallbackDel) As Long
End Function
Public Delegate Sub AlertManCallbackDel(ByVal data As IntPtr)
Public Sub AlertManCallback(ByVal data As IntPtr)
End Sub
Edit
I updated the code based on the native signature you posted. Can you try this out?
Your callback should look like this:
Public Delegate Sub AlertManCallbackDel(ByRef data As Byte)
The reason for this being that you are passing a single-byte value by reference.
As for the declaration of the unmanaged function, it should look like this:
<DllImport("AlertMan.dll")> _
Public Shared Function AlertManC( _
ByVal CallbackAddr As AlertManCallbackDel) As Integer
End Function
Note that the return type is an Integer, which in VB.NET is a 32-bit value. In VB6, a Long was a 32-bit value, hence the need for a change in VB.NET.
The callback definition is important to get right as well, btw.
If the callback's calling convention is cdecl, you cant do that directly in C# or VB.NET.
You will have to modify the IL of the delegate to behave correctly.
You can search on CodeProject for an in-depth article.
Update:
I guess not the correct answer :) But will leave my response.