Programmatic Execution Excel Macro on Remote Machine from a website - c#

I have a website where users generate an Excel report using a macro, when I try to run it in my local machine it generates perfectly and runs the macro inside Excel. When I publish it into the server and at the same time I am logged in there (RDP open session) and try to run it from a browser outside that server it is also running as expected. The problem occurs when I am logged off in the server (RDP) then run it in a browser outside the server (ie from my machine) the macro does not run but creates my Excel.
This is the code that I am using
public class Report
{
protected Workbook Workbook { get; set; }
protected Application Excel { get; set; }
public void RunReport()
{
// Launch Excel on the server
Excel = new Application
{
DisplayAlerts = false,
ScreenUpdating = false,
Visible = false
};
// Load the workbook template
Workbook = Excel.Workbooks.Open(#"D:\Book1.xlt");
// Execute macros that generates the report, if any
ExecuteMacros();
Workbook.SaveAs(#"D:\Ray'sTesting.xls", XlFileFormat.xlExcel8);
QuitExcel();
}
private void QuitExcel()
{
if (Workbook != null)
{
Workbook.Close(false);
Marshal.ReleaseComObject(Workbook);
}
if (Excel != null)
{
Excel.Quit();
Marshal.ReleaseComObject(Excel);
}
}
private void ExecuteMacros()
{
const string legacyModuleName = "Module1";
const string legacyMacroName = "myMacro";
bool legacyMacroExists = false;
try
{
var legacyMacroModule = Workbook.VBProject.VBComponents.Item(legacyModuleName);
if (legacyMacroModule != null)
{
int legacyMacroStartLine = legacyMacroModule.CodeModule.ProcStartLine[legacyMacroName, Microsoft.Vbe.Interop.vbext_ProcKind.vbext_pk_Proc];
legacyMacroExists = legacyMacroStartLine > 0;
}
}
catch (Exception)
{
legacyMacroExists = false;
}
if (!legacyMacroExists)
{
return;
}
// VBA code for the dynamic macro that calls the CI2 legacy macro
var moduleCode = new StringBuilder();
moduleCode.AppendLine("Public Sub LaunchLegacyMacro()");
moduleCode.AppendLine(string.Format("{0}.{1}", legacyModuleName, legacyMacroName));
moduleCode.AppendLine("End Sub");
// Add the dynamic macro to the ThisWorkbook module
var workbookMainModule = Workbook.VBProject.VBComponents.Item("ThisWorkbook");
workbookMainModule.CodeModule.AddFromString(moduleCode.ToString());
// Execute the dynamic macro
Microsoft.VisualBasic.Interaction.CallByName(Workbook, "LaunchLegacyMacro", Microsoft.VisualBasic.CallType.Method, new object[] { });
}
}

I got this working by editing the registry every time we run the excel macro by
private static void ModifyExcelSecuritySettings()
{
// Make sure we have programmatic access to the project to run macros
using (var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(#"Software\Microsoft\Office\14.0\Excel\Security", true))
{
if (key != null)
{
if ((int)key.GetValue("AccessVBOM", 0) != 1)
{
key.SetValue("AccessVBOM", 1);
}
key.Close();
}
}
}
So the code should look like this
public void RunReport()
{
ModifyExcelSecuritySettings();
// Launch Excel on the server
Excel = new Application
{
DisplayAlerts = false,
ScreenUpdating = false,
Visible = false
};
.....
I also created a blog post for the full solution which you can view here
http://anyrest.wordpress.com/2012/06/22/programmatic-execution-excel-macro-on-remote-machine-from-a-website/

Related

C# Prevent users from losing data in excel interop

I am writing an application in C# that changes data in an excel worksheet. VBA isn't an option because the version of office that is installed on the clients desktop is Microsoft Office 2010 Starter Edition which doesn't support VBA (none of the starter editions do). The application is using the excel interop library.
When I start the application it checks to see if the excel workbook that is to be modified is open and if it is open it notifies the user and then quits. This part is working as expected. The check isn't working if the user opens the excel file for some reason after starting the application and then trying to save their work from inside the application. In that case any modifications from the application are lost without any error notification. If you need to see more of the code to answer the entire project is in GitHub.
I've tried changing CheckExcelWorkBookOpen from a static class to a class that gets instantiated every time it is used, just in case the list of open workbooks was being stored in the excel interop library, this did not help.
The code that works in the application start up is:
CheckExcelWorkBookOpen testOpen = new CheckExcelWorkBookOpen();
testOpen.TestAndThrowIfOpen(Preferences.ExcelWorkBookFullFileSpec);
The code is also called any time the application attempts to open the file either for input or output, this doesn't work:
private void StartExcelOpenWorkbook()
{
if (xlApp != null)
{
return;
}
CheckExcelWorkBookOpen testOpen = new CheckExcelWorkBookOpen();
testOpen.TestAndThrowIfOpen(WorkbookName);
xlApp = new Excel.Application();
xlApp.Visible = false;
xlApp.DisplayAlerts = false;
xlWorkbook = xlApp.Workbooks.Open(WorkbookName);
}
Current Code
using System;
using Excel = Microsoft.Office.Interop.Excel;
namespace TenantRosterAutomation
{
public class CheckExcelWorkBookOpen
{
// Check if there is any instance of excel open using the workbook.
public static bool IsOpen(string workBook)
{
Excel.Application TestOnly = null;
bool isOpened = true;
// There are 2 possible exceptions here, GetActiveObject will throw
// an exception if no instance of excel is running, and
// workbooks.get_Item throws an exception if the sheetname isn't found.
// Both of these exceptions indicate that the workbook isn't open.
try
{
TestOnly = (Excel.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Excel.Application");
int lastSlash = workBook.LastIndexOf('\\');
string fileNameOnly = workBook.Substring(lastSlash + 1);
TestOnly.Workbooks.get_Item(fileNameOnly);
TestOnly = null;
}
catch (Exception)
{
isOpened = false;
if (TestOnly != null)
{
TestOnly = null;
}
}
return isOpened;
}
// Common error message to use when the excel file is op in another app.
public string ReportOpen()
{
string alreadyOpen = "The excel workbook " +
Globals.Preferences.ExcelWorkBookFullFileSpec +
" is alread open in another application. \n" +
"Please save your changes in the other application and close the " +
"workbook and then try this operation again or restart this application.";
return alreadyOpen;
}
public void TestAndThrowIfOpen(string workBook)
{
if (IsOpen(workBook))
{
AlreadyOpenInExcelException alreadOpen =
new AlreadyOpenInExcelException(ReportOpen());
throw alreadOpen;
}
}
}
}
This code is now included in a question on code review.
I got the above code to work by ensuring that any excel process started by the application was killed after the task was complete. The following code is added to my ExcelInterface module. The Dispose(bool) function already existed but did not kill the process:
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
if (xlWorkbook != null)
{
xlWorkbook.Close();
xlWorkbook = null;
}
if (xlApp != null)
{
xlApp.Quit();
xlApp = null;
Process xlProcess = Process.GetProcessById(ExcelProcessId);
if (xlProcess != null)
{
xlProcess.Kill();
}
}
}
disposed = true;
}
}
[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
private int GetExcelProcessID(Excel.Application excelApp)
{
int processId;
GetWindowThreadProcessId(excelApp.Hwnd, out processId);
return processId;
}
private void StartExcelOpenWorkbook()
{
if (xlApp != null)
{
return;
}
CheckExcelWorkBookOpen testOpen = new CheckExcelWorkBookOpen();
testOpen.TestAndThrowIfOpen(WorkbookName);
xlApp = new Excel.Application();
xlApp.Visible = false;
xlApp.DisplayAlerts = false;
xlWorkbook = xlApp.Workbooks.Open(WorkbookName);
ExcelProcessId = GetExcelProcessID(xlApp);
}

Kill a process that opened a file, after the file is closed by the user [WPF application]

I am trying to efficiently open-close-reopen a power bi file (.pbix) from a WPF application button click. My method starts by creating a process that opens the pbix file then kills the process when the file is closed and then when the button is clicked again creates a new process to re-open the file.
Kindly find below the code I use to execute the steps above.
namespace TestApp
{
public class MainWindowViewModel : INotifyPropertyChanged
{
public int CheckFileIsOpen(string filenamepath)
{
try
{
using FileStream fs = new FileStream(filenamepath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
return 0;
}
catch (Exception)
{
WindowEffect = new BlurEffect();
Mouse.OverrideCursor = null;
bool? Result = new CustomMessageBox($"File: {filenamepath.Split(#"\").Last()} in use!\nClose it and try again.", "File used by another process", MessageType.Error, MessageButtons.Ok).ShowDialog(); //this is a MessageBox object
if (Result.Value)
{
WindowEffect = null;
return 1;
}
else
{
WindowEffect = null;
return 2;
}
}
}
private void OpenOnlineLocally(bool open_local)
{
Process p = new Process();
string copy_name = "File_Copy.pbix";
string path = AppDomain.CurrentDomain.BaseDirectory; //the directory the .exe file runs.
try
{
Mouse.OverrideCursor = Cursors.Wait;
if (open_local == true)
{
int IsPBIFileOpen = CheckFileIsOpen($#"{path}{copy_name}");
if (new[] { 1, 2 }.Contains(IsPBIFileOpen))
{
return;
}
//Open the file using the system process
p.StartInfo = new ProcessStartInfo($"{path}{copy_name}")
{
UseShellExecute = true
};
p.Start();
}
else
{
OpenUrl("https://app.powerbi.com/...");
}
}
finally
{
if (p.HasExited) { p.Kill(); } //kill the process if the user closed the .pbix file
}
}
public ICommand ExportPowerBICommand //binded to a button click command in xaml
{
get { return new DelegateCommand<object>(FuncExportPowerBI); }
}
public void FuncExportPowerBI(object parameter)
{
Mouse.OverrideCursor = Cursors.Wait;
try
{
OpenOnlineLocally(true);
}
finally
{
Mouse.OverrideCursor = null;
}
}
}
}
The above code generates this error in the finally statement:
System.InvalidOperationException: 'No process is associated with this object.'
Some notes after experimentation:
The process should be killed when the user closes the .pbix file (i.e. clicks the X icon on top right corner of the desktop app). If the process is not killed and the user re-clicks the button to re-open the file then I get an error that the file is already opened and used by another process.
I prefer to avoid a solution that uses process.WaitForExit(), for two reasons. First, the application freezes while the file is used by the user. Second, it takes a couple of seconds for the desktop to realize that the process has exited so it can kill() it (not time efficient).
Since you're running .NET 5, there's an asynchronous method Process.WaitForExitAsync(). Async operation will not block the UI.
I've made the changes to two methods
private async Task OpenOnlineLocally(bool open_local)
{
Process p = new Process();
string copy_name = "File_Copy.pbix";
string dir = AppDomain.CurrentDomain.BaseDirectory; //the directory the .exe file runs.
string path = Path.Combine(dir, copy_name);
try
{
if (open_local == true)
{
int IsPBIFileOpen = CheckFileIsOpen(path);
if (IsPBIFileOpen != 0)
{
return;
}
//Open the file using the system process
p.StartInfo = new ProcessStartInfo(path)
{
UseShellExecute = true
};
p.Start();
await p.WaitForExitAsync();
}
else
{
OpenUrl("https://app.powerbi.com/...");
}
}
finally
{
if (!p.HasExited) { p.Kill(); } //kill the process if the user closed the .pbix file
}
}
public async void FuncExportPowerBI(object parameter)
{
Mouse.OverrideCursor = Cursors.Wait;
try
{
await OpenOnlineLocally(true);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message); // handle possible exception here
}
Mouse.OverrideCursor = null;
}

Set Expiry for Offline setup Application

Primarily am new to Developing.
I have Built a Setup File for my c# web Application using VS 2017. Am Going to Deploy my Setup in client Computer with Expiry Date which will be stored in registry. My setup can work in offline. But i want to collect/gather Client data weekly once. So What all i want is , let my client to use my setup without internet for one week. after that my setup have to become expired. Then he/she must connect with internet to extend an expiry date for Next one week. Kindly Help Me to achieve this. Thanks in advance
First of all create new form for read/write/check registry for check registration date and check with current date and add that for main form (first opening form, for this you can change form name in program.cs) , also you can add method in current main form and run that method in load events, after check you can chose between start program or show dialog for connect and extend or etc..
small code :
public partial class Reg_Form : Form
{
private string Reg_path = "Software\\MySampleProgram";
private string Reg_key = "Expire";
public Reg_Form()
{
InitializeComponent();
check_expire_date();
}
private void check_expire_date()
{
bool exists_key = check_reg();
if (!exists_key)
{
//first use, so we need to write expire date
bool is_write = write_reg();
if (!is_write)
{
MessageBox.Show("Run program as administrator and try again.");
this.Close();
}
}
var dt = read_reg_val();
if (dt != null)
{
DateTime current_date = DateTime.UtcNow;
DateTime reg_date = DateTime.UtcNow;
DateTime.TryParse(dt, out reg_date);
var res = (current_date - reg_date).TotalDays;
if (res < 7)
{
Main_Form frm = new Main_Form();
this.Hide();
frm.Show();
}
else
{
MessageBox.Show("Please connect to internet for extend time !");
//do any other work.....
}
}
else
{
MessageBox.Show("Error, try again !");
this.Close();
}
}
private bool write_reg()
{
try
{
Microsoft.Win32.RegistryKey key;
key = Microsoft.Win32.Registry.LocalMachine.CreateSubKey(Reg_path);
key.SetValue(Reg_key, DateTime.UtcNow.ToShortDateString());
key.Close();
return true;
}
catch
{
return false;
}
}
private bool check_reg()
{
try
{
RegistryKey key = Registry.LocalMachine.OpenSubKey(Reg_path);
if (key != null)
{
Object o = key.GetValue(Reg_key);
if (o != null)
{
return true;
}
}
}
catch
{
return false;
}
return false;
}
private string read_reg_val()
{
try
{
RegistryKey key = Registry.LocalMachine.OpenSubKey(Reg_path);
if (key != null)
{
Object o = key.GetValue(Reg_key);
if (o != null)
{
string res = o as String;
DateTime dt = DateTime.Now;
bool ok_date = DateTime.TryParse(res, out dt);
if (ok_date)
{
return res;
}
else
{
//changed value by user or hacked !
DateTime today = DateTime.UtcNow;
DateTime sevenDaysEarlier = today.AddDays(-8);
return sevenDaysEarlier.ToShortDateString();
}
}
}
}
catch
{
return null;
}
return null;
}
}
Note : this is note perfect, user can change the time & date on PC Or if can find registry can change and bypass expiry date, however it's good sample for you.
if problem solved, so please don't forget to mark answer as accepted/solved.

Single instance dotnetcore cli app on linux

I am interested in how to inforce a single instance policy for dotnetcore console apps. To my surprise it seems like there isn't much out there on the topic. I found this one stacko, How to restrict a program to a single instance, but it doesnt seem to work for me on dotnetcore with ubuntu. Anyone here do this before?
Variation of #MusuNaji's solution at: How to restrict a program to a single instance
private static bool AlreadyRunning()
{
Process[] processes = Process.GetProcesses();
Process currentProc = Process.GetCurrentProcess();
logger.LogDebug("Current proccess: {0}", currentProc.ProcessName);
foreach (Process process in processes)
{
if (currentProc.ProcessName == process.ProcessName && currentProc.Id != process.Id)
{
logger.LogInformation("Another instance of this process is already running: {pid}", process.Id);
return true;
}
}
return false;
}
This is a little more difficult on .NET core than it should be, due to the problem of mutex checking on Linux/MacOS (as reported above). Also Theyouthis's solution isn't helpful as all .NET core apps are run via the CLI which has a process name of 'dotnet' which if you are running multiple .NET core apps on the same machine the duplicate instance check will trigger incorrectly.
A simple way to do this that is also multi-platform robust is to open a file for write when the application starts, and close it at the end. If the file fails to open it is due to another instance running concurrently and you can handle that in the try/catch. Using FileStream to open the file will also create it if it doesn't first exist.
try
{
lockFile = File.OpenWrite("SingleInstance.lck");
}
catch (Exception)
{
Console.WriteLine("ERROR - Server is already running. End that instance before re-running. Exiting in 5 seconds...");
System.Threading.Thread.Sleep(5000);
return;
}
Here is my implementation using Named pipes. It supports passing arguments from the second instance.
Note: I did not test on Linux or Mac but it should work in theory.
Usage
public static int Main(string[] args)
{
instanceManager = new SingleInstanceManager("8A3B7DE2-6AB4-4983-BBC0-DF985AB56703");
if (!instanceManager.Start())
{
return 0; // exit, if same app is running
}
instanceManager.SecondInstanceLaunched += InstanceManager_SecondInstanceLaunched;
// Initialize app. Below is an example in WPF.
app = new App();
app.InitializeComponent();
return app.Run();
}
private static void InstanceManager_SecondInstanceLaunched(object sender, SecondInstanceLaunchedEventArgs e)
{
app.Dispatcher.Invoke(() => new MainWindow().Show());
}
Your Copy-and-paste code
public class SingleInstanceManager
{
private readonly string applicationId;
public SingleInstanceManager(string applicationId)
{
this.applicationId = applicationId;
}
/// <summary>
/// Detect if this is the first instance. If it is, start a named pipe server to listen for subsequent instances. Otherwise, send <see cref="Environment.GetCommandLineArgs()"/> to the first instance.
/// </summary>
/// <returns>True if this is tthe first instance. Otherwise, false.</returns>
public bool Start()
{
using var client = new NamedPipeClientStream(applicationId);
try
{
client.Connect(0);
}
catch (TimeoutException)
{
Task.Run(() => StartListeningServer());
return true;
}
var args = Environment.GetCommandLineArgs();
using (var writer = new BinaryWriter(client, Encoding.UTF8))
{
writer.Write(args.Length);
for (int i = 0; i < args.Length; i++)
{
writer.Write(args[i]);
}
}
return false;
}
private void StartListeningServer()
{
var server = new NamedPipeServerStream(applicationId);
server.WaitForConnection();
using (var reader = new BinaryReader(server, Encoding.UTF8))
{
var argc = reader.ReadInt32();
var args = new string[argc];
for (int i = 0; i < argc; i++)
{
args[i] = reader.ReadString();
}
SecondInstanceLaunched?.Invoke(this, new SecondInstanceLaunchedEventArgs { Arguments = args });
}
StartListeningServer();
}
public event EventHandler<SecondInstanceLaunchedEventArgs> SecondInstanceLaunched;
}
public class SecondInstanceLaunchedEventArgs
{
public string[] Arguments { get; set; }
}
Unit test
[TestClass]
public class SingleInstanceManagerTests
{
[TestMethod]
public void SingleInstanceManagerTest()
{
var id = Guid.NewGuid().ToString();
var manager = new SingleInstanceManager(id);
string[] receivedArguments = null;
var correctArgCount = Environment.GetCommandLineArgs().Length;
manager.SecondInstanceLaunched += (sender, e) => receivedArguments = e.Arguments;
var instance1 = manager.Start();
Thread.Sleep(200);
var manager2 = new SingleInstanceManager(id);
Assert.IsFalse(manager2.Start());
Thread.Sleep(200);
Assert.IsTrue(instance1);
Assert.IsNotNull(receivedArguments);
Assert.AreEqual(correctArgCount, receivedArguments.Length);
var receivedArguments2 = receivedArguments;
var manager3 = new SingleInstanceManager(id);
Thread.Sleep(200);
Assert.IsFalse(manager3.Start());
Assert.AreNotSame(receivedArguments, receivedArguments2);
Assert.AreEqual(correctArgCount, receivedArguments.Length);
}
}
The downside of deandob's solution is that one can launch the application from another path. So you may prefer some static path or a tmp path for all users.
Here is my attempt:
//second instance launch guard
var tempPath = Environment.GetEnvironmentVariable("TEMP", EnvironmentVariableTarget.Machine)
??
Path.GetTempPath();
var lockPath = Path.Combine(tempPath, "SingleInstance.lock");
await using var lockFile = File.OpenWrite(lockPath);
here I'm trying to get TEMP system variable at the scope of machine (not the user TEMP) and if its empty - fallback to the user's temp folder on windows or shared /tmp on some linuxes.

Setup & Deployment Project different then Source

I created this program that works fine in VS12 and compiles and runs fines. However I created a setup and deployment project for it using the InstallShield wizard. The installation is successfull on my test machine however I can not get my EventHandler working it is a service instance that i parse the events on.
class EventHandler : EventHandlerService.EventHandlerCCServicePortType
{
public EventHandlerService.handleEventResponse1 handleEvent(EventHandlerService.handleEventRequest eventMsg)
{
EventHandlerService.handleEventResponse1 resp = new EventHandlerService.handleEventResponse1();
EventHandlerService.handleEventResponse respInfo = new EventHandlerService.handleEventResponse();
resp.handleEventResponse = respInfo;
respInfo.#return = true;
Main instance = Main.getFormInstance();
instance.setPhoneStatus(eventMsg.handleEvent.#event);
return (resp);
}
}
static public Main getFormInstance()
{
return (instance);
}
delegate void setPhoneStatusCB(EventHandlerService.standardEventMsg eventMsg);
public void setPhoneStatus(EventHandlerService.standardEventMsg eventMsg)
{
DisplayEvent(eventMsg);
if (eventMsg.eventType == EventHandlerService.standardEventMsgEventType.CALL_EVENT)
{
if (eventMsg.callEvent.objectId == Int64.Parse(Peridot.Properties.Settings.Default.OIGPhoneObjID.ToString()))
{
Peridot.Properties.Settings.Default.OIGPhoneLastEvent = eventMsg.callEvent.type.ToString();
if (eventMsg.callEvent.callState.ToString().Length > 0)
{
Peridot.Properties.Settings.Default.OIGPhoneState = eventMsg.callEvent.callState.ToString();
}
if (eventMsg.callEvent.localCallId.ToString().Length > 0)
{
Peridot.Properties.Settings.Default.OIGPhoneCallId = eventMsg.callEvent.localCallId.ToString();
writeToLogFile("Getting OIG Phone Call ID EventMsg: " + eventMsg.callEvent.localCallId.ToString(), "SYSTEM");
}
else
{
Peridot.Properties.Settings.Default.OIGPhoneCallId = "";
writeToLogFile("Getting OIG Phone Call ID Failied! EventMsg: " + eventMsg.callEvent.localCallId.ToString(), "SYSTEM");
}
}
}
}
}
Seems like setPhoneStatus never gets called and the Instance is never sent, this only happens on the deployed program and not in the development. Any Ideas?

Categories