Background
I'm currently working on an application in VSTO2015 and Excel 2016. The application manages a number of CustomTaskPanes in different windows. I am trying to get // some code to fire when the task pane is opened or closed. In order to handle the various windows, I've implemented a structure very similar to this;
CustomTaskPane in Excel doesn't appear in new Workbooks
ThisAddIn.cs contains the following class;
public class TaskPaneManager
{
static Dictionary<string, Microsoft.Office.Tools.CustomTaskPane> _createdPanes = new Dictionary<string, Microsoft.Office.Tools.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 Microsoft.Office.Tools.CustomTaskPane GetTaskPane(string taskPaneId, string taskPaneTitle, Func<UserControl> taskPaneCreatorFunc)
{
string key = string.Format("{0}({1})", taskPaneId, Globals.ThisAddIn.Application.Hwnd);
string title = taskPaneId;
string windowId = Globals.ThisAddIn.Application.Hwnd.ToString();
if (!_createdPanes.ContainsKey(key))
{
var customTaskPane = taskPaneCreatorFunc();
var pane = Globals.ThisAddIn.CustomTaskPanes.Add(customTaskPane, taskPaneTitle);
_createdPanes[key] = pane;
//
// Set the task pane width as set in the App.Config
//
pane.Width = Convert.ToInt32(ConfigurationManager.AppSettings["TaskPaneWidth"]);
}
return _createdPanes[key];
}
....
My calls from Ribbon.cs;
private void btnUploadWizard_Click(object sender, RibbonControlEventArgs e)
{
// Check for configuration sheet first.
string title = "Upload Wizard";
TaskPaneManager.isConfigurationCreated();
var UploadWizardTaskpane = TaskPaneManager.GetTaskPane(title, title, () => new TaskPaneUploadWizard());
UploadWizardTaskpane.Visible = !UploadWizardTaskpane.Visible;
}
The Problem: Event Handlers
I'm having difficulty getting an event handler to fire. I can't tell what I'm doing wrong. In the TaskPaneDesigner I am attaching the event using this.VisibleChanged += new System.EventHandler(this.TaskPaneUploadWizard_VisibleChanged);, and then defining it in my TaskPaneUploadWizard class as follows;
public partial class TaskPaneUploadWizard : UserControl
{
...
public TaskPaneUploadWizard()
{
InitializeComponent();
}
private void TaskPaneUploadWizard_VisibleChanged(object sender, EventArgs e)
{
// Some code
}
My thoughts
It seems to me as though I am either attaching the eventHandler to something other than the CustomTaskPane object, or I am attaching it before the CustomTaskPane is created.
Help!
To detect if the task pane was opened or closed, you have to attach to the VisibleChanged event of pane.
So the simplest solution would be to add just one line of code to the GetTaskPane method:
var pane = Globals.ThisAddIn.CustomTaskPanes.Add(customTaskPane, taskPaneTitle);
// This is the new line to be added.
pane.VisibleChanged += (s, e) => customTaskPane.Visible = pane.Visible;
_createdPanes[key] = pane;
Now the visibility of the whole task pane will be passed on to its content and // some code should be executed.
Alternatively, if you don't want to manually set customTaskPane.Visible for whatever reason, you could as well execute your code directly in this new event handler:
pane.VisibleChanged += (s, e) => { /* some code */ };
But personally I would rather recommend the first approach because it seems to fit a bit better into your existing code.
var UploadWizardTaskpane = TaskPaneManager.GetTaskPane(title, title, () => new TaskPaneUploadWizard());
This is creating a CustomTaskPane not a TaskPaneUploadWizard object
It seems like this.VisibleChanged += new System.EventHandler(this.TaskPaneUploadWizard_VisibleChanged); is acting on TaskPaneUploadWizard which is only the guest of CustomTaskPane
Note that The visibility of CustomTaskPane doesn't affect TaskPaneUploadWizard
My suggestion
You remove VisibleChanged from the designer, you will add it manually in your TaskPaneUploadWizard
public partial class TaskPaneUploadWizard : UserControl
{
//{---}
CustomTaskPane _HostPane;
public CustomTaskPane HostPane
{
get => _HostPane;
set
{
if(_HostPane == value)
return;
_HostPane?.VisibleChanged -= TaskPaneUploadWizard_VisibleChanged;
_HostPane = value;
_HostPane?.VisibleChanged += TaskPaneUploadWizard_VisibleChanged;
}
}
//{---}
private void TaskPaneUploadWizard_VisibleChanged(object sender, EventArgs e)
{
// Some code
}
//{---}
Then in GetTaskPane you say
//{---}
var pane = Globals.ThisAddIn.CustomTaskPanes.Add(customTaskPane, taskPaneTitle);
(customTaskPane as TaskPaneUploadWizard).HostPane = pane;
//{---}
Related
I'm having trouble manipulating forms when from another thread.
I've overcome the issue by loading the form at runtime, showing it then hiding it. This means the form is created on the right thread and can be manipulated using invokes.
This is not the right way to do it. I have 3 problems that come from using this method
I can't spawn another popup box I have to use the one I created at runtime
The forms flash briefly on load - now that I have 3 forms its pretty obvious what I'm doing.
I have to use a variable bool to hold if the popup is open or not.
If anyone could point me in the right direction It would be much appreciated. Currently my code looks like:
On Main form Load:
CallerIDfrm = new frmCallerID();
CallerIDfrm.Show();
CallerIDfrm.Hide();
to manipulate the popup Im using
delegate void StringArgReturningVoidDelegate1(string CallerIDnum, string CallerIDname, string ContactID);
private void CallerID(string CallerIDnum, string CallerIDname, string ContactID)
{
if (CallerIDfrm.InvokeRequired)
{
StringArgReturningVoidDelegate1 d = new StringArgReturningVoidDelegate1(CallerID);
CallerIDfrm.Invoke(d, new object[] { CallerIDnum, CallerIDname, ContactID });
}
else
{
if (ContactID != null || ContactID != "0")
{
CallerIDfrm.ContactID = ContactID;
}
CallerIDfrm.Mainfrm = this;
CallerIDfrm.TopLevel = true;
CallerIDfrm.TopMost = true;
CallerIDfrm.lblCallerIDname.Text = CallerIDname;
CallerIDfrm.lblCallerIDnum.Text = CallerIDnum;
CallerIDfrm.Show();
CallerIDOpen = true;
}
}
To Hide the popup until required again im using:
delegate void StringArgReturningVoidDelegate2();
private void CallerIDClose()
{
if (CallerIDfrm.InvokeRequired)
{
StringArgReturningVoidDelegate2 d = new StringArgReturningVoidDelegate2(CallerIDClose);
CallerIDfrm.Invoke(d, new object[] { });
}
else
{
try
{
CallerIDfrm.Hide();
CallerIDOpen = false;
}
catch
{
}
}
}
I've tried otherways but the Popup loads as if it is not responding and I loose access to the popup.
Ultimately I'd like to be able to spawn multiple popups and have the ability to close them from the Main Form.
What I gather from your question: You have an caller api/lib/class and you like to show CallerId on a popup form when a call is received. Have a look at Events and Event Driven programming.
The following codes has not been tested, I wrote it from top of my head. Might not compile, they are here to show an example:
Create an CallReceived event in api/lib class as follows:
public event EventHandler<CallReceivedEventArgs> CallReceived;
protected void OnCallReceived(EventArgs e)
{
var handler = CallReceived;
if (handler != null)
handler(this, e);
// Note: For C# 6.0 and later, above statements can be simplified to
// CallReceived?.Invoke(this, e);
}
Note: If you don't have access to this api/lib code, create a Gateway class and put your event in there along with mechanism to trigger it.
Also create a CallReceivedEventArgs, this will be used to transfer event data:
public class CallReceivedEventArgs : EventArgs
{
public string CallerIDnum {get; set;}
public string CallerIDname {get; set;}
public string ContactID {get; set;}
}
Now, in your api/lib class raise this event whenever a call is received:
// a call received, replace dummy values with actual values
OnCallReceived(new CallReceivedEventArgs() { CallerIDnum="5554443322", CallerIDname="SOME_NAME", ContactID="SOME_CONTACT" });
Finally in your GUI form, register to said event and process accordingly.
// inside your main form class
private CallerAPI callerApi = new CallerAPI();
// somewhere inside you main form class, register to event
// from your use case, I would place it inside Main Form's constructor
callerApi.CallReceived += callerApi_Callreceived;
// receive event
void callerApi_Callreceived(object sender, CallReceivedEventArgs e)
{
var callerIDnum = e.CallerIDnum;
// etc.
// show callerId form with details
// you need to change frmCallerID's constructor accordingly
CallerIDfrm = new frmCallerID(e.CallerIDnum, CallerIDname, ContantID);
// to be able to track opened popups, you can store them inside a list
// private List<Form> openPopupList = new List<Form>();
//
// alternatively, you can assign CallerIDnum to form's name property
// and store these inside a List<string> instead of List<Form>
openPopupList.add(CallerIDfrm);
CallerIDfrm.Show();
}
Don't forget to unregister from event.
callerApi.CallReceived -= callerApi_Callreceived;
To wrap it up:
I can't spawn another popup box I have to use the one I created at runtime
You can create and show multiple frmCallerID, independent from each other.
The forms flash briefly on load - now that I have 3 forms its pretty obvious what I'm doing.
Since new approach creates CallerID forms based on events, you won't see these form flashing. It'll open whenever a CallReceived event is received.
I have to use a variable bool to hold if the popup is open or not.
A better approach would be: Register to forms FormClosed event, and remove from openPopupList accordingly.
frmCallerID.FormClosed += frmCallerID_FormClosed;
void frmCallerID_FormClosed(object sender, EventArgs e)
{
// remove form from openPopupList
frmCallerID closedPopup = (frmCallerID) sender;
openPopupList.remove(closedPopup);
}
I have created a PowerPoint VSTO Addin with a custom Task pane - and a ribbon where a toggle button defines the display / hide Status of the custom Task pane. Basis for this was the Microsoft Walkthrough information for custom Task pane and synchronizing the Ribbon with the Task pane.
So fare everything works fine with the first PowerPoint window. I'm able to show the Task pane in the second and third PowerPoint window, but the toggle button on the ribbon only reacts to the last opened / created PowerPoint window and not to the Task pane displayed / hidded in the active PowerPoint window.
I've found another thread which explains exactly the same Problem here:
C# VSTO-Powerpoint-TaskPanes in separate windows.
But I don't understand the answer neither I don't know how to implement a PowerPoint Inspector Wrapper.
I'm new in C# and just getting a keyword like "Inspector Wrapper" is to less for me. I already spend hours in searching the net but wasn't successfull till now.
Is there a chance to get a COMPLETE code example for PowerPoint how this works, what has to be done?
Code added:
I took the code from the General walkthrough: https://msdn.microsoft.com/en-us/library/bb608590.aspx and changed it with an Event for new presentations:
The code for the ThisAddIn.cs is as follow:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using PowerPoint = Microsoft.Office.Interop.PowerPoint;
using Office = Microsoft.Office.Core;
namespace PowerPointAddIn1
{
public partial class ThisAddIn
{
private TaskPaneControl taskPaneControl1;
private Microsoft.Office.Tools.CustomTaskPane taskPaneValue;
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
this.Application.AfterNewPresentation += new Microsoft.Office.Interop.PowerPoint.EApplication_AfterNewPresentationEventHandler(NewPresentation);
//taskPaneControl1 = new TaskPaneControl();
//taskPaneValue = this.CustomTaskPanes.Add( taskPaneControl1, "MyCustomTaskPane");
//taskPaneValue.VisibleChanged += new EventHandler(taskPaneValue_VisibleChanged);
}
void NewPresentation(Microsoft.Office.Interop.PowerPoint.Presentation oPres)
{
PowerPoint.Application app = this.Application;
PowerPoint.DocumentWindow docWin = null;
foreach (PowerPoint.DocumentWindow win in Globals.ThisAddIn.Application.Windows)
{
if (win.Presentation.Name == app.ActivePresentation.Name)
{
docWin = win;
}
}
this.taskPaneControl1 = new TaskPaneControl();
this.taskPaneValue = this.CustomTaskPanes.Add(taskPaneControl1, "MyCustomTaskPane", docWin);
this.taskPaneValue.VisibleChanged += new EventHandler(taskPaneValue_VisibleChanged);
}
private void taskPaneValue_VisibleChanged(object sender, System.EventArgs e)
{
Globals.Ribbons.ManageTaskPaneRibbon.toggleButton1.Checked =
taskPaneValue.Visible;
}
public Microsoft.Office.Tools.CustomTaskPane TaskPane
{
get
{
return taskPaneValue;
}
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
}
#region VSTO generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
}
I remember the learning curve oh so well. Here's a sample that I believe addresses your issue. You need to link the task pane to the document. I relied on the naming scheme for new documents here, but a DocumentVariable would be a much better choice (they are discarded at the end of the current session). Add a variable to the presentation, store the task pane id in the control, and compare them to get the right task pane.
You need an XML ribbon (could probably use a Ribbon Designer but those are not as good). I removed some of the boilerplate and irrelevant code from this.
ThisAddIn.cs:
namespace PowerPointAddIn1
{
public partial class ThisAddIn
{
public static int counter = 0;
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
this.Application.AfterNewPresentation += Application_AfterNewPresentation;
}
private void Application_AfterNewPresentation(PowerPoint.Presentation Pres)
{
int count = ++counter;
UserControl1 uc = new UserControl1("task pane " + count);
CustomTaskPane ctp = CustomTaskPanes.Add(uc, "custom task pane " + count);
ctp.Visible = true;
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
}
protected override Microsoft.Office.Core.IRibbonExtensibility CreateRibbonExtensibilityObject()
{
return new Ribbon1();
}
}
}
Ribbon1.cs:
namespace PowerPointAddIn1
{
[ComVisible(true)]
public class Ribbon1 : Office.IRibbonExtensibility
{
private Office.IRibbonUI ribbon;
public Ribbon1()
{
}
public void toggleTaskPane(Office.IRibbonControl control, bool enabled)
{
var CTPs = Globals.ThisAddIn.CustomTaskPanes;
var pres = Globals.ThisAddIn.Application.ActivePresentation;
foreach (var x in CTPs)
{
if (pres.Name.EndsWith(x.Title.Replace("custom task pane ", "")))
{
x.Visible = enabled;
}
}
}
public bool isPressed(Office.IRibbonControl control)
{
var CTPs = Globals.ThisAddIn.CustomTaskPanes;
var pres = Globals.ThisAddIn.Application.ActivePresentation;
foreach (var x in CTPs)
{
if (pres.Name.EndsWith(x.Title.Replace("custom task pane ", "")))
{
return x.Visible;
}
}
return false;
}
}
}
Ribbon1.xml:
<?xml version="1.0" encoding="UTF-8"?>
<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="Ribbon_Load">
<ribbon>
<tabs>
<tab idMso="TabAddIns">
<group id="MyGroup"
label="My Group">
<checkBox id="mycheckbox" label="show task pane" onAction="toggleTaskPane" getPressed="isPressed" />
</group>
</tab>
</tabs>
</ribbon>
</customUI>
UsreControl1.cs (just has a label on it):
namespace PowerPointAddIn1
{
public partial class UserControl1 : UserControl
{
public UserControl1(string labelValue)
{
InitializeComponent();
label1.Text = labelValue;
}
}
}
I just want to share my results which works now for me (Thanks to Chris who gave me some valuable inputs). I do have a customtaskpane management which works for each presentation. The only Thing which is not yet implemented is if a user opens the document in a separate window (View / New Window). This one I don't know how to manage.
As fare as I can test it this works now.
here is the link to the whole solution:
https://happypc-my.sharepoint.com/personal/roger_heckly_happy-pc_ch/_layouts/15/guestaccess.aspx?docid=0426d40dc5df74d66ba42a3b928111ce8&authkey=Aa6yX6QWUnqXp1jcUfGveL8
Please be Aware - I'm beginner - so if you have feedback / inputs please let me know. For sure, some code could be written easier etc.
I'm developing a Windows Forms App, a connector between a callCenter and a CRM and I'm clogged with an unhandled exception which I can't understand or either solve.
My App has:
MainForm with some controls and a FlowControll Pannel which may or may not show a collection of SearchForms - OK
SearchForm - basically a DataGridView with clickable cells - depending on the column index, the click will perform different operations - OK
DialForm - loads as consequence of clicking a cell on my SearchForm and allows to i) Cancel (closes the form OK); ii) dial a number -PROBLEM - very frustrating
When I click to dial, the app correctly makes the phoneCall trough my callCenter, but the very next line of code (Dispose() upon the DialForm) generates an unhandled exception of type 'Safe Handle has been closed', reportedly with DangerousAddRef(Boolean& success).
The relevant methods:
///
/// DialForm Method - upon clicking «Dial Number» Button
///
private void dialButton_Click(object sender, EventArgs e)
{
//
// Piece of code to manage closing with DialFormCloseEventArgs
//
DialFormCloseEventArgs args = new DialFormCloseEventArgs();
args.toClose = this;
EventHandler<DialFormCloseEventArgs> eh = father.Search_CloseDialForm;
this.BeginInvoke(eh, new object[] { sender, args });
}
///
/// SearchForm Method - EventHandler to close DialForm and make call
///
public void Search_CloseDialForm(object sender, DialFormCloseEventArgs e)
{
string numberToDial = e.toClose.numberToDial.Text;
e.toClose.Dispose();
this.rePositionMainForm();
using (ConectorCTI.ConectorCTI ctiws = new ConectorCTI.ConectorCTI())
{
ctiws.Timeout = 180000;
// Synchronous Call
//ctiws.Dial(this.userLoginName, numberToDial, "");
// Assynchronous Call
ctiws.DialAsync(this.userLoginName, numberToDial, "");
}
}
I also show my App with textual descriptions so you can actually «see it»
Probably need to wait for the async task to complete before disposing of the object. Something like
using (ConectorCTI.ConectorCTI ctiws = new ConectorCTI.ConectorCTI())
{
var task = ctiws.DialAsync(this.userLoginName, numberToDial, "");
await task;
}
In my main window, I create a thread, which is executing a while() loop. The main tasks have two parts: receive data from socket and show it on GUI.
Now I need to show the data on another window at the same time. So I create it first like below.
ShowForm showForm = new ShowForm();
public MainWindow()
{
InitializeComponent();
mainThread();
showForm.Show();
}
And send the data to the showForm like below: (coordinateValue is generated within main window)
showForm.setter(coordinateValue);
And in the code of ShowForm.Designer.cs:
int xValue;
public void setter(int val)
{
xValue = val;
}
Now I don't know how to show the xValue on the showForm repeatedly (needs to be updated timely), e.g. a textBox or convert the xValue to coordinate and show it on a pictureBox. And in the meanwhile, the main Window's while() loop should continue to receive data and show it on its GUI.
You can create an event in your MainWindow. And subscribe to that event in your ShowForm.
Than whenever your data changes MainWindow should raise that event. Just remember that if you get the data in another thread you can't just pass it to the GUI which runs on the main thread. In that case you will want to use a Dispatcher.
You might want to use the Timer class. It can execute methods (Tick event) in even intervals.
I have wrote a sample that explains how to transfer data from one form to another using an event. If it runs in another thread you should use Invoke method in your control to prevent errors.
public partial class AdditionalForm : Form
{
private Label l_dataToShow;
public Label DataToShow { get { return l_dataToShow; } }
public AdditionalForm()
{
InitializeComponent();
SuspendLayout();
l_dataToShow = new Label();
l_dataToShow.AutoSize = true;
l_dataToShow.Location = new Point(12, 9);
l_dataToShow.Size = new Size(40, 13);
l_dataToShow.TabIndex = 0;
l_dataToShow.Text = "Data will be shown here";
Controls.Add(l_dataToShow);
ResumeLayout();
}
}
public partial class MainForm : Form
{
private AdditionalForm af;
public MainForm()
{
InitializeComponent();
SuspendLayout();
txtbx_data = new TextBox();
txtbx_data.Location = new System.Drawing.Point(12, 12);
txtbx_data.Name = "txtbx_data";
txtbx_data.Size = new System.Drawing.Size(100, 20);
txtbx_data.TabIndex = 0;
Controls.Add(txtbx_data);
ResumeLayout();
txtbx_data.TextChanged += new EventHandler(txtbx_data_TextChanged);
af = new AdditionalForm();
af.Show();
}
/// <summary>
/// The data that contains textbox will be transfered to another form to a label when you will change text in a textbox. You must make here your own event that will transfer data.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void txtbx_data_TextChanged(object sender, EventArgs e)
{
af.DataToShow.Text = txtbx_data.Text;
}
}
I must say I am pretty stumped by this and I hope some of you have perhaps encountered the same issue. I have a HyperlinkButton, created like this:
var hb = new HyperlinkButton
{
Content = fooText,
ClickMode = ClickMode.Release,
NavigateUri = new Uri(fooLink.Value)
};
hb.Click += hb_Click;
static void hb_Click(object sender, RoutedEventArgs e)
{
var btn = (HyperlinkButton)sender;
HtmlPage.Window.Navigate(btn.NavigateUri, "_blank");
}
The link is an absolute URL to a website (http://...), not inside the application. Therefore, I want to open it in a new browser tab. Which is exactly what it does! The website opens in a new tab, but in the application tab I get this error:
If I change the event handler so that there's no target parameter, like this:
HtmlPage.Window.Navigate(btn.NavigateUri);
...the error appears as well, just before the browser navigates away from the application. But I want to open a new tab.
I am using the Silverlight Business Application template from Visual Studio 11 and I've already noticed it has a few mystery bugs, this might be one of them. All in all, it works, I just need to get rid of the error message. But there is no exception thrown in the event handler that I could swallow.
You could do just this:
var hb = new HyperlinkButton
{
Content = fooText,
ClickMode = ClickMode.Release,
NavigateUri = new Uri(fooLink.Value),
TargetName = "_blank"
};
without the Click event handler.
It should do exactly what you want, in a cleaner way too.
Try it with this:
/// <summary>
/// Hyperlink button - simulates hyperlink click
/// </summary>
private class HyperlinkButtonWrapper : HyperlinkButton
{
public void OpenURL(string navigateUri)
{
OpenURL(new Uri(navigateUri, UriKind.Absolute));
}
public void OpenURL(Uri navigateUri)
{
base.NavigateUri = navigateUri;
base.TargetName = "_blank";
base.OnClick();
}
}
/// <summary>
/// Method opens url
/// <para>Example: OpenURL("http://www.google.com")</para>
/// </summary>
/// <param name="navigateUri"></param>
public static void OpenURL(string navigateUri)
{
new HyperlinkButtonWrapper().OpenURL(navigateUri);
}