VSTO Word post save event - c#

Sorry for the length of this, In the past I've been asked before to included everything I've tried when asking these sort of questions.
I'm writing a Word Add-in and need to make changes to the document that I cannot achieve using the Word object model. Therefore, after the document is save to disk, I need to capture that event, close the file, do what I need to do and reopen it. (I know, not elegant, but that's what I have to work with.)
Word has Before Save and Before Close, but no after save event. I found tricks online to simulate an after save event by creating another thread and using COM's IMessageFilter (not from System.Windows.Forms) to handle COM Retry calls, or posting a message back on the main thread so I can execute code after it's saved. But this doesn't work because if the file is saved as a result of the user attempting to close the document I can't get the file name in my "callback" method because Word.Document object has already been deleted.
So I tried explicitly call Save myself in my BeforeSave event handler and return Cancel = true. That works great when user selects save, or they had once saved to disk. But if the user closes a new document without saving and then selects 'yes' to whether or not they want to save, Word displays another "SaveAs" dialog after I've already handled the save after I return from the BeforeSave event, even though I set Cancel = true in my BeforeSave event handler.
So then I tried doing the something similar with the BeforeClose event. I handle the close and save myself and then returning Cancel = true from my event handler. But doing so stops word from trying to close multiple documents when the user is attempting to shut down the application.
I even tried handling WM_CLOSE, but that lead to similar problems as above.
Can anyone offer a solution?

I came across this a while back, I think it may do what you want. Here's a copy of what's there in case it ever disappears.
When I wrote my first Word AfterSave Event entry, it was designed for Word 2007, and was – as it turns out – not a catch all. So I have updated it here (thanks for the catch go to Pat Lemm).
When the document was closed, you never got access to the Saved filename. So, I have updated the code here and it now works in all conditions and has been tested in Word 2013.
Here is how it works:
Upon initialization you pass it your Word object.
It attaches to the Before Save Event.
When any save event occurs, it kicks off a thread that loops until the Background Save is complete.
Once the background save is done, it checks to see if the document Saved == true:
If Saved == true: then a regular save did occur.
If Saved == false: then it had to be an AutoSave
In each case it will fire a unique event:
AfterSaveUiEvent
AfterSaveEvent
AfterAutoSaveEvent
Additionally, if the document being saved is also being closed, we catch the filename on the WindowDeactivate event on the way out. This can now be accessed by the caller (as you can see in the example below), to get the full filename of the closed document.
Here is the code to the class:
public class WordSaveHandler
{
public delegate void AfterSaveDelegate(Word.Document doc, bool isClosed);
// public events
public event AfterSaveDelegate AfterUiSaveEvent;
public event AfterSaveDelegate AfterAutoSaveEvent;
public event AfterSaveDelegate AfterSaveEvent;
// module level
private bool preserveBackgroundSave;
private Word.Application oWord;
string closedFilename = string.Empty;
/// <summary>
/// CONSTRUCTOR takes the Word application object to link to.
/// </summary>
/// <param name="oApp"></param>
public WordSaveHandler(Word.Application oApp)
{
oWord = oApp;
// hook to before save
oWord.DocumentBeforeSave += oWord_DocumentBeforeSave;
oWord.WindowDeactivate += oWord_WindowDeactivate;
}
/// <summary>
/// Public property to get the name of the file
/// that was closed and saved
/// </summary>
public string ClosedFilename
{
get
{
return closedFilename;
}
}
/// <summary>
/// WORD EVENT fires before a save event.
/// </summary>
/// <param name="Doc"></param>
/// <param name="SaveAsUI"></param>
/// <param name="Cancel"></param>
void oWord_DocumentBeforeSave(Word.Document Doc, ref bool SaveAsUI, ref bool Cancel)
{
// This could mean one of four things:
// 1) we have the user clicking the save button
// 2) Another add-in or process firing a resular Document.Save()
// 3) A Save As from the user so the dialog came up
// 4) Or an Auto-Save event
// so, we will start off by first:
// 1) Grabbing the current background save flag. We want to force
// the save into the background so that Word will behave
// asyncronously. Typically, this feature is on by default,
// but we do not want to make any assumptions or this code
// will fail.
// 2) Next, we fire off a thread that will keep checking the
// BackgroundSaveStatus of Word. And when that flag is OFF
// no know we are AFTER the save event
preserveBackgroundSave = oWord.Options.BackgroundSave;
oWord.Options.BackgroundSave = true;
// kick off a thread and pass in the document object
bool UiSave = SaveAsUI; // have to do this because the bool from Word
// is passed to us as ByRef
new Thread(() =>
{
Handle_WaitForAfterSave(Doc, UiSave);
}).Start();
}
/// <summary>
/// This method is the thread call that waits for the same to compelte.
/// The way we detect the After Save event is to essentially enter into
/// a loop where we keep checking the background save status. If the
/// status changes we know the save is compelte and we finish up by
/// determineing which type of save it was:
/// 1) UI
/// 2) Regular
/// 3) AutoSave
/// </summary>
/// <param name="Doc"></param>
/// <param name="UiSave"></param>
private void Handle_WaitForAfterSave(Word.Document Doc, bool UiSave)
{
try
{
// we have a UI save, so we need to get stuck
// here until the user gets rid of the SaveAs dialog
if (UiSave)
{
while (isBusy())
Thread.Sleep(1);
}
// check to see if still saving in the background
// we will hang here until this changes.
while (oWord.BackgroundSavingStatus > 0)
Thread.Sleep(1);
}
catch (ThreadAbortException)
{
// we will get a thread abort exception when Word
// is in the process of closing, so we will
// check to see if we were in a UI situation
// or not
if (UiSave)
{
AfterUiSaveEvent(null, true);
}
else
{
AfterSaveEvent(null, true);
}
}
catch
{
oWord.Options.BackgroundSave = preserveBackgroundSave;
return; // swallow the exception
}
try
{
// if it is a UI save, the Save As dialog was shown
// so we fire the after ui save event
if (UiSave)
{
// we need to check to see if the document is
// saved, because of the user clicked cancel
// we do not want to fire this event
try
{
if (Doc.Saved == true)
{
AfterUiSaveEvent(Doc, false);
}
}
catch
{
// DOC is null or invalid. This occurs because the doc
// was closed. So we return doc closed and null as the
// document
AfterUiSaveEvent(null, true);
}
}
else
{
// if the document is still dirty
// then we know an AutoSave happened
try
{
if (Doc.Saved == false)
AfterAutoSaveEvent(Doc, false); // fire autosave event
else
AfterSaveEvent(Doc, false); // fire regular save event
}
catch
{
// DOC is closed
AfterSaveEvent(null, true);
}
}
}
catch { }
finally
{
// reset and exit thread
oWord.Options.BackgroundSave = preserveBackgroundSave;
}
}
/// <summary>
/// WORD EVENT – Window Deactivate
/// Fires just before we close the document and it
/// is the last moment to get the filename
/// </summary>
/// <param name="Doc"></param>
/// <param name="Wn"></param>
void oWord_WindowDeactivate(Word.Document Doc, Word.Window Wn)
{
closedFilename = Doc.FullName;
}
/// <summary>
/// Determines if Word is busy essentially that the File Save
/// dialog is currently open
/// </summary>
/// <param name="oApp"></param>
/// <returns></returns>
private bool isBusy()
{
try
{
// if we try to access the application property while
// Word has a dialog open, we will fail
object o = oWord.ActiveDocument.Application;
return false; // not busy
}
catch
{
// so, Word is busy and we return true
return true;
}
}
}
And here is how you set it up and attach to it’s events:
public partial class ThisAddIn
{
WordSaveHandler wsh = null;
private void ThisAddIn_Startup(object sender,
System.EventArgs e)
{
// attach the save handler
wsh = new WordSaveHandler(Application);
wsh.AfterAutoSaveEvent += new WordSaveHandler.AfterSaveDelegate(wsh_AfterAutoSaveEvent);
wsh.AfterSaveEvent += new WordSaveHandler.AfterSaveDelegate(wsh_AfterSaveEvent);
wsh.AfterUiSaveEvent += new WordSaveHandler.AfterSaveDelegate(wsh_AfterUiSaveEvent);
}
void wsh_AfterUiSaveEvent(Word.Document doc, bool isClosed)
{
if (!isClosed)
MessageBox.Show("After SaveAs Event");
else
MessageBox.Show("After Close and SaveAs Event. The filname was: " + wsh.ClosedFilename);
}
void wsh_AfterSaveEvent(Word.Document doc, bool isClosed)
{
if (!isClosed)
MessageBox.Show("After Save Event");
else
MessageBox.Show("After Close and Save Event. The filname was: " + wsh.ClosedFilename);
}
void wsh_AfterAutoSaveEvent(Word.Document doc, bool isClosed)
{
MessageBox.Show("After AutoSave Event");
}
// etc.
}

Related

how can i get this c# server side wait window to quit when exporting is finished?

Team!!
I have a website that let's people download data into an excel or pdf file. The download takes long enough that I want to display a "Please Wait" modal while the processing is happening. I found some code that does this, but the problem is it just counts from 1 to 100 and then quits. I'd like to alter that so that it displays until the download is complete.
When the use clicks the button to begin the export, the expot_click function is called. The WorkerMethod function below it gets passed into the class and is where the count from 1 to 100 is occurring. I'm not sure what to change, or where to change it.
protected void export_click(object sender, EventArgs e)
{
string exportType = hidExportType.Value;
string exportSections = hidSections.Value;
string exportStudents = hidStudents.Value;
//Display a "Please Wait" message while the download is happening
object result = WaitWindow.Show(this.WorkerMethod);
switch (exportType)
{
case "csv":
export_csv(exportSections, exportStudents);
break;
case "excel":
export_excel(exportSections, exportStudents);
break;
case "pdf":
export_pdf(exportSections, exportStudents);
break;
default:
break;
}
//I'm guessing here is where I'd want to do something to make the wait display quit?
}
private void WorkerMethod(object sender, Jacksonsoft.WaitWindowEventArgs e)
{
// Do something
for (int progress = 1; progress <= 100; progress++)
{
System.Threading.Thread.Sleep(20);
// Update the wait window message
e.Window.Message = string.Format
("Please wait ... {0}%", progress.ToString().PadLeft(3));
}
// Use the arguments sent in
if (e.Arguments.Count > 0)
{
// Set the result to return
e.Result = e.Arguments[0].ToString();
}
else
{
// Set the result to return
e.Result = "Hello World";
}
}
Below is the class that gets called:
/*
* Created by SharpDevelop.
* User: mjackson
* Date: 05/03/2010
* Time: 09:36
*
* To change this template use Tools | Options | Coding | Edit Standard Headers.
*/
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace Jacksonsoft
{
/// <summary>
/// Displays a window telling the user to wait while a process is executing.
/// </summary>
public class WaitWindow
{
/// <summary>
/// Shows a wait window with the text 'Please wait...' while executing the passed method.
/// </summary>
/// <param name="workerMethod">Pointer to the method to execute while displaying the wait window.</param>
/// <returns>The result argument from the worker method.</returns>
public static object Show(EventHandler<WaitWindowEventArgs> workerMethod){
return WaitWindow.Show(workerMethod, null);
}
/// <summary>
/// Shows a wait window with the specified text while executing the passed method.
/// </summary>
/// <param name="workerMethod">Pointer to the method to execute while displaying the wait window.</param>
/// <param name="message">The text to display.</param>
/// <returns>The result argument from the worker method.</returns>
public static object Show(EventHandler<WaitWindowEventArgs> workerMethod, string message){
WaitWindow instance = new WaitWindow();
return instance.Show(workerMethod, message, new List<object>());
}
/// <summary>
/// Shows a wait window with the specified text while executing the passed method.
/// </summary>
/// <param name="workerMethod">Pointer to the method to execute while displaying the wait window.</param>
/// <param name="message">The text to display.</param>
/// <param name="args">Arguments to pass to the worker method.</param>
/// <returns>The result argument from the worker method.</returns>
public static object Show(EventHandler<WaitWindowEventArgs> workerMethod, string message, params object[] args){
List<object> arguments = new List<object>();
arguments.AddRange(args);
WaitWindow instance = new WaitWindow();
return instance.Show(workerMethod, message, arguments);
}
#region Instance implementation
private WaitWindow(){}
private WaitWindowGUI _GUI;
internal delegate void MethodInvoker<T>(T parameter1);
internal EventHandler<WaitWindowEventArgs> _WorkerMethod;
internal List<object> _Args;
/// <summary>
/// Updates the message displayed in the wait window.
/// </summary>
public string Message{
set{
this._GUI.Invoke(new MethodInvoker<string>(this._GUI.SetMessage), value);
}
}
/// <summary>
/// Cancels the work and exits the wait windows immediately
/// </summary>
public void Cancel(){
this._GUI.Invoke(new MethodInvoker(this._GUI.Cancel), null);
}
private object Show(EventHandler<WaitWindowEventArgs> workerMethod, string message, List<object> args){
// Validate Parameters
if (workerMethod == null){
throw new ArgumentException("No worker method has been specified.", "workerMethod");
} else {
this._WorkerMethod = workerMethod;
}
this._Args = args;
if (string.IsNullOrEmpty(message)){
message = "Please wait...";
}
// Set up the window
this._GUI = new WaitWindowGUI(this);
this._GUI.MessageLabel.Text = message;
// Call it
this._GUI.ShowDialog();
object result = this._GUI._Result;
// clean up
Exception _Error = this._GUI._Error;
this._GUI.Dispose();
// Return result or throw and exception
if (_Error != null){
throw _Error;
} else {
return result;
}
}
#endregion Instance implementation
}
}
Any suggestions on what to do to get this working would be greatly appreciated!!
You can't use a server side window.
but, what you can do is pop or display a dialog WHEN the user clicks on the button. That pop dialog will stay open while the web page is up on the server being processed. When the server page is done, then that new fresh server page is then send back to the client side browser and thus the display message will go away on its own.
So, say your button to start process is a simple button. When clicked on that "longer" server side code runs.
So, you keep the buttion, but add a client side script to pop up a dialog message.
In this case we will use a jQuery.UI dialog.
So, you will need in the web page jQquery, and jQuery.UI.
So, now the simple button code becomes this:
<asp:Button ID="cmdBigProcess" runat="server" Text="Start the reactor!"
OnClientClick="showait();return true;"
OnClick="cmdBigProcess_Click" />
<div id="mywait" style="display:none;border:solid">
<h2>Big process running - please wait</h2>
</div>
<script>
function showait() {
// lets pop jquery.UI dialog
var mydiv = $("#mywait")
mydiv.dialog({
modal: true, appendTo: "form",
title: "Please wait", closeText: "",
width: "400px"
});
}
</script>
So, we added a simple "div" that will hold the message.
Now, when you click on the button, the "on client click" code runs FIRST, and displays the dialog, then your server side code runs. As noted, we don't have to dismiss or do anything to the dialog, since when the web page is posted, it travels up to the server. The code behind runs (may or may not change thing on the web page), and THEN when the page is done, the whole web page now travels back down to the web side, browser re-plots and re-loads the web page (this is the so called round trip, or often called the page life-cycle. it is BEYOND critical that you grasp and understand this round trip concept. Your code behind NEVER directly interacts with the user. But upon hitting a buttion, the WHOLE web page travels up to server, code runs. And EVEN when the code modifies the controls and things in the web page? They will NOT show up, NOT update, and user will not see any changes to that web page until ALL YOUR code behind is done. Once code behind is done, then the WHOLE page makes the trip back to browser and is re-loaded.
So, the re-load of the page is what in fact will case the popped up dialog to be dismissed. Note that jQuery.UI dialogs (and most web dialogs) do NOT halt code when called - code continues.
The result looks like this:

Why Could My Application Trial Not be Working in the Marketplace

I recently have released a small application with a trial to the Windows Phone Marketplace, but my application is not working as expected. I have followed http://code.msdn.microsoft.com/Trial-Experience-Sample-c58f21af when making my trial, so that I can call the current 'LicenseInformation' state and block a feature or not depending on the current application's license state. According to the sample application, The LicenseMode property returns a value from the LicenseModes enum (Full, MissingOrRevoked, or Trial) so that your app code needs to check only a single value. There’s also a convenient Boolean IsFull property. Whenever the license mode has changed, or it is likely to have changed, TrialExperienceHelper raises its LicenseChanged event and your app code can handle that event to query LicenseMode or IsFull again. Then, your app can control the availability of features, ads, and your Buy UI as needed.
In my application I have a click event in which I would like to perform an action based on the current LicenseInformation state and upon a count (the count being the number of times an image is saved with particular aspects applied).
Settings.SavedCount.Value records the number of times the save button is clicked, and if the count is above 100 and the application is in trial mode I would like to ask the user if they would like to upgrade, otherwise if the count is less than 100 while the application is in trial mode or if the license is in full mode then the user is allowed to continue with the save process (hopefully that makes logical sense).
void saveButton_Click(object sender, EventArgs e)
{
Settings.SavedCount.Value += 1;
if (TrialViewModel.LicenseModeString == "Trial" && Settings.SavedCount.Value > 100)
{
MessageBoxResult result = MessageBox.Show("You have saved over 100 items! Would you like to continue?", "Congratulations!", MeesageBoxButton.OKCancel);
switch (result)
{
case MessageBoxResult.OK:
//A command takes a parameter so pass null
TrialViewModel.BuyCommand.Execute(null);
break;
case MessageBoxResult.Cancel:
editPagePivotControl.SelectedIndex = 0;
break;
}
}
else if ((TrialViewModel.LicenseModeString == "Trial" && Settings.SavedCount.Value <= 100) || (TrialViewModel.LicenseModeString == "Full")
{
PerformSaveAsync();
}
}
}
When testing in Debug mode and with the sample implementation from the msdn website, the Trial and Full implementations worked properly, and then when in Release mode the license was listed as MissingOrRevoked which I assumed would be called correctly in the marketplace. What is ACTUALLY occuring when i have downloaded the app in the marketplace under both trial and full modes is that the PerformSaveAsync() method is never being called (which ultimately saves the new image and disables the button) and I can use the new image elsewhere. I am having trouble figuring out what the issue may be?
EDIT** In researching I came across http://msdn.microsoft.com/en-us/library/aa691310(v=vs.71).aspx which states that The operation x && y corresponds to the operation x & y, except that y is evaluated only if x is true. and `•The operation x || y corresponds to the operation x | y, except that y is evaluated only if x is false/' . Would this be the cause of the issues? If so, how should they be fixed?
Edit 2** Addtion of TrialViewModel and TrialExperienceHelper.cs for additional info
TrialViewModel
TrialViewModel
#region fields
private RelayCommand buyCommand;
#endregion fields
#region constructors
public TrialViewModel()
{
// Subscribe to the helper class's static LicenseChanged event so that we can re-query its LicenseMode property when it changes.
TrialExperienceHelper.LicenseChanged += TrialExperienceHelper_LicenseChanged;
}
#endregion constructors
#region properties
/// <summary>
/// You can bind the Command property of a Button to BuyCommand. When the Button is clicked, BuyCommand will be
/// invoked. The Button will be enabled as long as BuyCommand can execute.
/// </summary>
public RelayCommand BuyCommand
{
get
{
if (this.buyCommand == null)
{
// The RelayCommand is constructed with two parameters - the action to perform on invocation,
// and the condition under which the command can execute. It's important to call RaiseCanExecuteChanged
// on a command whenever its can-execute condition might have changed. Here, we do that in the TrialExperienceHelper_LicenseChanged
// event handler.
this.buyCommand = new RelayCommand(
param => TrialExperienceHelper.Buy(),
param => TrialExperienceHelper.LicenseMode == TrialExperienceHelper.LicenseModes.Trial);
}
return this.buyCommand;
}
}
public string LicenseModeString
{
get
{
return TrialExperienceHelper.LicenseMode.ToString()/* + ' ' + AppResources.ModeString*/;
}
}
#endregion properties
#region event handlers
// Handle TrialExperienceHelper's LicenseChanged event by raising property changed notifications on the
// properties and commands that
internal void TrialExperienceHelper_LicenseChanged()
{
this.RaisePropertyChanged("LicenseModeString");
this.BuyCommand.RaiseCanExecuteChanged();
}
#endregion event handlers
TrialExperienceHelper.cs
#region enums
/// <summary>
/// The LicenseModes enumeration describes the mode of a license.
/// </summary>
public enum LicenseModes
{
Full,
MissingOrRevoked,
Trial
}
#endregion enums
#region fields
#if DEBUG
// Determines how a debug build behaves on launch. This field is set to LicenseModes.Full after simulating a purchase.
// Calling the Buy method (or navigating away from the app and back) will simulate a purchase.
internal static LicenseModes simulatedLicMode = LicenseModes.Trial;
#endif // DEBUG
private static bool isActiveCache;
private static bool isTrialCache;
#endregion fields
#region constructors
// The static constructor effectively initializes the cache of the state of the license when the app is launched. It also attaches
// a handler so that we can refresh the cache whenever the license has (potentially) changed.
static TrialExperienceHelper()
{
TrialExperienceHelper.RefreshCache();
PhoneApplicationService.Current.Activated += (object sender, ActivatedEventArgs e) => TrialExperienceHelper.
#if DEBUG
// In debug configuration, when the user returns to the application we will simulate a purchase.
OnSimulatedPurchase();
#else // DEBUG
// In release configuration, when the user returns to the application we will refresh the cache.
RefreshCache();
#endif // DEBUG
}
#endregion constructors
#region properties
/// <summary>
/// The LicenseMode property combines the active and trial states of the license into a single
/// enumerated value. In debug configuration, the simulated value is returned. In release configuration,
/// if the license is active then it is either trial or full. If the license is not active then
/// it is either missing or revoked.
/// </summary>
public static LicenseModes LicenseMode
{
get
{
#if DEBUG
return simulatedLicMode;
#else // DEBUG
if (TrialExperienceHelper.isActiveCache)
{
return TrialExperienceHelper.isTrialCache ? LicenseModes.Trial : LicenseModes.Full;
}
else // License is inactive.
{
return LicenseModes.MissingOrRevoked;
}
#endif // DEBUG
}
}
/// <summary>
/// The IsFull property provides a convenient way of checking whether the license is full or not.
/// </summary>
public static bool IsFull
{
get
{
return (TrialExperienceHelper.LicenseMode == LicenseModes.Full);
}
}
#endregion properties
#region methods
/// <summary>
/// The Buy method can be called when the license state is trial. the user is given the opportunity
/// to buy the app after which, in all configurations, the Activated event is raised, which we handle.
/// </summary>
public static void Buy()
{
MarketplaceDetailTask marketplaceDetailTask = new MarketplaceDetailTask();
marketplaceDetailTask.ContentType = MarketplaceContentType.Applications;
marketplaceDetailTask.Show();
}
/// <summary>
/// This method can be called at any time to refresh the values stored in the cache. We re-query the application object
/// for the current state of the license and cache the fresh values. We also raise the LicenseChanged event.
/// </summary>
public static void RefreshCache()
{
TrialExperienceHelper.isActiveCache = CurrentApp.LicenseInformation.IsActive;
TrialExperienceHelper.isTrialCache = CurrentApp.LicenseInformation.IsTrial;
TrialExperienceHelper.RaiseLicenseChanged();
}
private static void RaiseLicenseChanged()
{
if (TrialExperienceHelper.LicenseChanged != null)
{
TrialExperienceHelper.LicenseChanged();
}
}
#if DEBUG
private static void OnSimulatedPurchase()
{
TrialExperienceHelper.simulatedLicMode = LicenseModes.Full;
TrialExperienceHelper.RaiseLicenseChanged();
}
#endif // DEBUG
#endregion methods
#region events
/// <summary>
/// The static LicenseChanged event is raised whenever the value of the LicenseMode property has (potentially) changed.
/// </summary>
public static event LicenseChangedEventHandler LicenseChanged;
#endregion events
If your dev builds work and the only difference is with the app being published via the store then I think it's very unlikely to be your logic.
When you submitted the app, are you sure you checked the option to be able to use the trial functionality in the app?
If you didn't check this then it won't work in a released app.
Regarding your edit, I don't see any problem with your condition, your quote is just that the operator is lazy, evaluating only what is needed to determine the result (for example when you do x&& y if x is false, x&& false=> false and x&& true==false which is the same result so it don't evaluate y).
Also like I said in your previous question even the windows phone 7 api still work on windows phone 8 so if you are creating code for both platform there is probably no need to use the new api specifically for wp8.
In this code I don't see any problem but why do you convert the LicenseModes enum to string, using the enum will add some type safety and prevent you to do some invalid comparison.
The only problem is where you set LicenseModeString or a problem inside PerformSaveAsync?

CustomTaskPane in Excel doesn't appear in new Workbooks

I've added a CustomTaskPane to Excel 2013 that lets users quickly search for photos. It works well and fine if the user only opens/creates one workbook. Problem is if they open another workbook or create a new one, the task pane doesn't appear in the new window that appears. It simply stays put in the original window. I know this behavior is caused by the fact I was only initializing the panel upon opening Excel. I added an event handler to the ActiveWindow event to initialize a new panel when they open another workbook.
Problem is I cannot figure out how to tell if the CustomTaskPane is already present in a window already. If it is, it simple creates another CustomTaskPane, so there are now two within that window. I wrote the following code to dispose the original and create a new one, but it introduces some lag (1-5 seconds) that would drive users crazy every time they change workbook windows. Is there a way to see if a CustomTaskPane already exists in a window to avoid disposing and recreating a new one to avoid stacking duplicate task panes?
Microsoft.Office.Tools.CustomTaskPane PartPhotoTaskPane;
Globals.ThisAddIn.Application.WindowActivate += Application_WindowActivate;
void Application_WindowActivate(Excel.Workbook Wb, Excel.Window Wn)
{
if (PartPhotoTaskPane != null)
{
PartPhotoTaskPane.Dispose();
InitalizePartPhotoViewerTaskPane(EPPF);
}
else
{
InitalizePartPhotoViewerTaskPane(EPPF);
}
}
/// <summary>
/// Start up the part photo viewer task pane
/// </summary>
private void InitalizePartPhotoViewerTaskPane(ExcelPartPhotoFunctions _EPPF)
{
//intialize the part search
try
{
PartPhotoTaskPane = Globals.ThisAddIn.CustomTaskPanes.Add(new PartPhotoSearchPane(_EPPF), "Part Information", Globals.ThisAddIn.Application.ActiveWindow);
PartPhotoTaskPane.Visible = Properties.Settings.Default.InfoPaneOpenStatus;
PartPhotoTaskPane.Width = 260;
}
catch (Exception e)
{
MessageBox.Show("Error starting Part Info Toolbar:" + Environment.NewLine +
e.Message + Environment.NewLine + e.StackTrace, "Error!", MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
Use the hwnd (Globals.ThisAddIn.Application.Hwnd) to identify the Excel window. This will work well both for Office2013 (which uses an SDI approach) and older versions of Office that use MDI windows. Here is a class you can use for this:
public class TaskPaneManager
{
static Dictionary<string, CustomTaskPane> _createdPanes = new Dictionary<string, CustomTaskPane>();
/// <summary>
/// Gets the taskpane by name (if exists for current excel window then returns existing instance, otherwise uses taskPaneCreatorFunc to create one).
/// </summary>
/// <param name="taskPaneId">Some string to identify the taskpane</param>
/// <param name="taskPaneTitle">Display title of the taskpane</param>
/// <param name="taskPaneCreatorFunc">The function that will construct the taskpane if one does not already exist in the current Excel window.</param>
public static CustomTaskPane GetTaskPane(string taskPaneId, string taskPaneTitle, Func<UserControl> taskPaneCreatorFunc)
{
string key = string.Format("{0}({1})", taskPaneId, Globals.ThisAddIn.Application.Hwnd);
if (!_createdPanes.ContainsKey(key))
{
var pane = Globals.ThisAddIn.CustomTaskPanes.Add(taskPaneCreatorFunc(), taskPaneTitle);
_createdPanes[key] = pane;
}
return _createdPanes[key];
}
}
Here I'm actually combining the Excel window hwnd and some arbitrary string identifier to identify the taskpane. The idea is to support multiple taskpanes in the same addin.
Here is an example for how to use it from the ribbon:
private void button1_Click(object sender, RibbonControlEventArgs e)
{
var taskpane = TaskPaneManager.GetTaskPane("A", "Task pane type A", () => new UserControl1());
taskpane.Visible = !taskpane.Visible;
}
private void button2_Click(object sender, RibbonControlEventArgs e)
{
var taskpane = TaskPaneManager.GetTaskPane("B", "Task pane type B", () => new UserControl2());
taskpane.Visible = !taskpane.Visible;
}
If you open multiple workbooks in Excel, both Excel window will have their own taspaneA and taskpaneB.

How to allow for multiple popups at once in WinRT?

If you call the ShowAsync command on a MessageDialog object when another MessageDialog object has already been displayed to the user but not dismissed (i.e. you show a popup when another one is already up), an UnauthorizedAccessException is thrown. This can make things difficult when you have multiple threads attempting to alert the user at the same time.
My current (stopgap) solution is merely to surround the ShowAsync call with a try/catch block and swallow the exception. This undesirably leads to the user missing out on subsequent notifications. The only other way around this that I can think of is to manually implement some sort of popup queue. This seems like an inordinate amount of work, however, considering other frameworks (like Windows Phone) do not have this issue and will merely display the popups one after another as the user dismisses them.
Is there another way to solve this problem?
You can easy do it with this extension method:
public static class MessageDialogShower
{
private static SemaphoreSlim _semaphore;
static MessageDialogShower()
{
_semaphore = new SemaphoreSlim(1);
}
public static async Task<IUICommand> ShowDialogSafely(this MessageDialog dialog)
{
await _semaphore.WaitAsync();
var result = await dialog.ShowAsync();
_semaphore.Release();
return result;
}
}
There are many ways to approach it and the choice might depend on your skills, requirements and preferences.
My personal choice is to avoid using dialog boxes altogether since they are bad for user experience (evil). There are then alternative solutions like displaying a separate screen/page with the UI requiring user to provide some input when it really is required or displaying a non-modal popup somewhere on the side/edge/corner if the user input is optional and hiding it after a moment or some other sort of notification that doesn't break user flow.
If you disagree or don't have the time, resources or skills to implement an alternative - you can create some sort of a wrapper around MessageDialog.ShowAsync() call to either queue or ignore new requests while a dialog is already shown.
This class has extension methods to allow to either ignore a new show request when another dialog is already displayed or queue up the requests:
/// <summary>
/// MessageDialog extension methods
/// </summary>
public static class MessageDialogExtensions
{
private static TaskCompletionSource<MessageDialog> _currentDialogShowRequest;
/// <summary>
/// Begins an asynchronous operation showing a dialog.
/// If another dialog is already shown using
/// ShowAsyncQueue or ShowAsyncIfPossible method - it will wait
/// for that previous dialog to be dismissed before showing the new one.
/// </summary>
/// <param name="dialog">The dialog.</param>
/// <returns></returns>
/// <exception cref="System.InvalidOperationException">This method can only be invoked from UI thread.</exception>
public static async Task ShowAsyncQueue(this MessageDialog dialog)
{
if (!Window.Current.Dispatcher.HasThreadAccess)
{
throw new InvalidOperationException("This method can only be invoked from UI thread.");
}
while (_currentDialogShowRequest != null)
{
await _currentDialogShowRequest.Task;
}
var request = _currentDialogShowRequest = new TaskCompletionSource<MessageDialog>();
await dialog.ShowAsync();
_currentDialogShowRequest = null;
request.SetResult(dialog);
}
/// <summary>
/// Begins an asynchronous operation showing a dialog.
/// If another dialog is already shown using
/// ShowAsyncQueue or ShowAsyncIfPossible method - it will wait
/// return immediately and the new dialog won't be displayed.
/// </summary>
/// <param name="dialog">The dialog.</param>
/// <returns></returns>
/// <exception cref="System.InvalidOperationException">This method can only be invoked from UI thread.</exception>
public static async Task ShowAsyncIfPossible(this MessageDialog dialog)
{
if (!Window.Current.Dispatcher.HasThreadAccess)
{
throw new InvalidOperationException("This method can only be invoked from UI thread.");
}
while (_currentDialogShowRequest != null)
{
return;
}
var request = _currentDialogShowRequest = new TaskCompletionSource<MessageDialog>();
await dialog.ShowAsync();
_currentDialogShowRequest = null;
request.SetResult(dialog);
}
}
Test
// This should obviously be displayed
var dialog = new MessageDialog("await ShowAsync", "Dialog 1");
await dialog.ShowAsync();
// This should be displayed because we awaited the previous request to return
dialog = new MessageDialog("await ShowAsync", "Dialog 2");
await dialog.ShowAsync();
// All other requests below are invoked without awaiting
// the preceding ones to complete (dialogs being closed)
// This will show because there is no dialog shown at this time
dialog = new MessageDialog("ShowAsyncIfPossible", "Dialog 3");
dialog.ShowAsyncIfPossible();
// This will not show because there is a dialog shown at this time
dialog = new MessageDialog("ShowAsyncIfPossible", "Dialog 4");
dialog.ShowAsyncIfPossible();
// This will show after Dialog 3 is dismissed
dialog = new MessageDialog("ShowAsyncQueue", "Dialog 5");
dialog.ShowAsyncQueue();
// This will not show because there is a dialog shown at this time (Dialog 3)
dialog = new MessageDialog("ShowAsyncIfPossible", "Dialog 6");
dialog.ShowAsyncIfPossible();
// This will show after Dialog 5 is dismissed
dialog = new MessageDialog("ShowAsyncQueue", "Dialog 7");
dialog.ShowAsyncQueue();
// This will show after Dialog 7 is dismissed
dialog = new MessageDialog("ShowAsyncQueue", "Dialog 8");
dialog.ShowAsyncQueue();

Notify Modal forms' parent that it needs to action something

I have a parent form open a modal form which basically allows a user to change the database settings of the application.
When the user clicks the save button on the modal (child) form it saves the Settings object with the new settings but I need to have the Main form check that the database settings are correct.
I currently do this through a function which tries to simply connect to the database and if successful return true, false if this failed. That function I execute within the applications constructor so it runs fine whenever the application is closed and restarted.
I tried the following within the modal form after saving the settings but get a NullReference exception for object myManager.
This is the function which gets the new settings and saves them and then attempts to call the parent forms CheckDatabaseIsSetup() public function to test the db connection.
/// <summary>
/// Save the settings and then hide the Settings window
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btn_Save_Click(object sender, EventArgs e)
{
// TRUE: User indicates that we are to connect using a trusted connection
// FALSE: User wants to use Integrated security to connect.
if (rb_UseTrustedConnection.Checked)
{
AppSettings.DatabaseName = tb_Trusted_DbName.Text;
AppSettings.Server = tb_Trusted_Server.Text;
AppSettings.UseIntergratedSecurity = false;
}
else
{
AppSettings.DatabaseName = tb_Secure_DbName.Text;
AppSettings.Server = tb_Secure_Server.Text;
AppSettings.Username = tb_Secure_Username.Text;
AppSettings.Password = tb_Secure_Password.Text;
AppSettings.UseIntergratedSecurity = true;
}
try
{
AppSettings.SaveSettings();
BushBreaksLodgeManagerMain myManager = (BushBreaksLodgeManagerMain)this.ParentForm;
myManager.CheckDatabaseIsSetup();
}
catch (Exception ex)
{
log.LogAppendWithException(ex);
}
this.Hide();
}
You should instead use the Owner property in the modal form rather than the ParentForm property as follows:
BushBreaksLodgeManagerMain myManager = (BushBreaksLodgeManagerMain)this.Owner;
The Owner property defines the actual relationship between the owned(modal) form and the parent(owner) form.
Its better to define an event in child form and handle this event in main form
and when ever you raise this event in child form mainform can does its own job
BushBreaksLodgeManagerMain myManager = (BushBreaksLodgeManagerMain)this.ParentForm;
You can check the above line whether ParentForm is of Type/can cast to BushBreaksLodgeManagerMain. I presume the case wasn't successful hence return null
I'd usually do this by using object intercommunication such as provided with my Emesary library; The design is to use notifications in such a way that the request is sent and handled by anything that knows it needs to process these notifications, so for example it's easy to add in extra event handlers that are disconnected.
In which case the code for checking the database settings would become:
if (ReceiptStatus.OK ==
GlobalNotifier.NotifyAll(new CheckDatabaseIsSetupNotification(tb_Secure_DbName.Text,
tb_Secure_Server.Text,
tb_Secure_Username.Text,
tb_Secure_Password.Text,
true))
{
// do something.
}
To make this work you'd need to implement IReceiver in BushBreaksLodgeManagerMain and in the constructor call
GlobalTransmitter.Register(this);
then implement the interface receive:
public ReceiptStatus Receive(INotification _message)
{
if (_message is CheckDatabaseIsSetupNotification)
{
var message = _message as CheckDatabaseIsSetupNotification;
if (connect_to(message.DatabaseName, message.Server, Message.Username, message.Password, message.UseIntergratedSecurity))
return ReceiptStatus.OK;
else
return ReceiptStatus.Fail;
}
return ReceiptStatus.NotProcessed;
}
You can do this using Windows events - but this way is clearer and allows inter operation with objects that don't necessarily have windows.

Categories

Resources