Custom Document property not getting saved in Word Document - c#

I have cretaed a add-in for word. I am trying to update value of a custom property in a word document on a button click. But its not getting saved.
The code I write is:
private void button_Click(object sender, IRibbonControl control, bool pressed)
{
Word.Document document = WordApp.ActiveDocument;
Microsoft.Office.Core.DocumentProperties properties;
properties = (Microsoft.Office.Core.DocumentProperties)document.CustomDocumentProperties;
properties["abc"].Value = "newValue";
document.Save();
}
Here if I close the document and open it again am getting the old value not the new one.
But if I add a space in my document and then save it. Then value of custom property getting saved.
Code is:
private void button_Click(object sender, IRibbonControl control, bool pressed)
{
Word.Document document = WordApp.ActiveDocument;
Microsoft.Office.Core.DocumentProperties properties;
properties = (Microsoft.Office.Core.DocumentProperties)document.CustomDocumentProperties;
properties["abc"].Value = "newValue";
document.Range(document.Content.End - 1, document.Content.End - 1).Select();
WordApp.Selection.Range.Text = " ";
document.Save();
}
Why the behavior is like this. I do not want to add any extra blank space in my document. Please help me in this. Thanks in advance.

This is a known "idiosyncracy" of a number of Office applications, not just Word. Changing the value of a document property, but nothing else, doesn't get "noticed", so it's not saved. There's quite a bit of detail in this discussion on MSDN.
Either the code needs to add something to the document "body" (which can then be deleted but not undone) or it can explicitly set the "dirty" flag on the document so that Word realizes it does need to save:
document.Saved = false;
document.Save();

Related

How do I use strings from XML as variable to create button? C#

To make this easier, I will show my code and then explain what im trying to do.
CODE+EXPLANATION:
First, when the user clicks on a button it goes to Other with additonal information.
private void button1_Click(object sender, EventArgs e)
=> Other("https://pastebin.com/raw/something", "Program1", "A");
private void button2_Click(object sender, EventArgs e)
=> Other("https://pastebin.com/raw/something", "Program2", "B");
Second, I download an XML document and extract neccesary information from it:
private void Other(string UniversalURL, string ProductNAME, string ProductCHANNEL)
{
//Download the Doc
XmlDocument document = new XmlDocument();
document.Load(UniversalURL);
string expandedEnvString = Environment.ExpandEnvironmentVariables("%USERPROFILE%/AppData/Local/Temp/zADU.xml");
File.WriteAllText(expandedEnvString, document.InnerXml);
XmlDocument doc = new XmlDocument();
doc.Load(expandedEnvString);
//Get the needed Nodes
XmlNode nodeXMLProgram = doc.DocumentElement.SelectSingleNode($"/{ProductNAME}");
string XMLProgram = nodeXMLProgram.InnerText;
// Creation of Button Here
}
GOAL: What I want to be able to do is use the strings, extracted from the XML and use them as variables in the creation of button, Kind of like this:
Button Program(XMLProgram) = new Button();
Program(XMLProgram).Height = 22;
Program(XMLProgram).Width = 122;
Program(XMLProgram).BackColor = Color.FromArgb(230, 230, 230);
Program(XMLProgram).ForeColor = Color.Black;
Program(XMLProgram).Name = "DynamicButton";
Program(XMLProgram).Click += new EventHandler(ProgramProgram(XMLProgram)_Click);
Program(XMLProgram).BringToFront();
Controls.Add(ProgramProgram(XMLProgram));
Would I be able to do this? Help would be appreciated! Sorry for confusing title, I don't know how to phrase it properly.
You can use Reflection to look for and set the properties of the object you are deserializing automatically, provided you have a properly formatted XML file.
Read your XML file through XDocument or XmlDocument, extract from it the type of control you need to create (button, textbox, etc), also extract the property names to set from the XML, and the values to set them to. Then create an instance of said control type, and use code like this to go through your property list from the XML and set them in your instance:
// New instance of the control (read the type from the XML and create accordingly)
var ctrlInstance = new Button();
// Get a reference to the type of the control created.
var ctrlType = ctrlInstance.GetType();
// Dictionary to contain property names and values to set (read from XML)
var properties = new Dictionary<string, object>();
foreach (var xmlProp in properties)
{
// Get a reference to the actual property in the type
var prop = ctrlType.GetProperty(xmlProp.Key);
if (prop != null && prop.CanWrite)
{
// If the property is writable set its value on the instance you created
// Note that you have to make sure the value is of the correct type
prop.SetValue(ctrlInstance, xmlProp.Value, null);
}
}
If you intend to create an entire program this way, including code, you will have to use Roslyn to compile your code at runtime, which can be somewhat complicated to put into practice. In that case, you don't need an XML file, you just need to put put all your code into a source file and compile it, then instantiate it in your own program. If you just want to create a form programmatically and handle events in its parent form, this will work fine.

How to use the Word RepeatingSection ContentControl in a protected word document

List item
I am implementing a Word template for a form filling application using VSTO and c# in Visual Studio 2017 and wish to take advantage of Word repeating section content control. However, I am being prevented from programmatically applying this type of control after I have previously protected the document for form filling. It appears that unprotecting the document does not return the document to the same unprotected state in this context as prior to protecting it. Here is a stripped down demonstration program to highlight the problem:
In Visual Studio create a new Word 2013 and 2016 VSTO Template project, leaving the project to use an unchanged default blank document template, add the following code to the ThisDocument partial class
private void ThisDocument_Startup(object sender, System.EventArgs e)
{
//Demonstrates an unexpected impact of protecting then subsequently unprotecting a document
AddTableDirect();
DocProtect();
DocUnprotect();
AddTableRepeatingSection();
}
private void ThisDocument_Shutdown(object sender, System.EventArgs e)
{
}
private void DocProtect()
{
//Protects the active document restricting the operator to form filling
object noReset = true;
object password = System.String.Empty;
object useIRM = false;
object enforceStyleLock = false;
this.Protect(Word.WdProtectionType.wdAllowOnlyFormFields,
ref noReset, ref password, ref useIRM, ref enforceStyleLock);
}
private void DocUnprotect()
{
// Unprotects the active document allowing programmatic manipulation
object password = System.String.Empty;
this.Unprotect(ref password);
}
private void AddTableDirect()
{
//Creates a one row table directly adding a single plain text content control
Word.Range range = this.Sections[1].Range.Paragraphs[1].Range;
Word.Table table = this.Tables.Add
(range, 1, 1, Word.WdDefaultTableBehavior.wdWord9TableBehavior, Word.WdAutoFitBehavior.wdAutoFitWindow);
Word.ContentControl cc = this.ContentControls.Add
(Word.WdContentControlType.wdContentControlText, table.Cell(1, 1).Range);
}
private void AddTableRepeatingSection()
{
//Programatically duplicates the table as a repeating section
Word.Table table = this.Sections[1].Range.Tables[1];
Word.Range rSRange = table.Range;
Word.ContentControl rSCC = this.ContentControls.Add
(Word.WdContentControlType.wdContentControlRepeatingSection, rSRange);
rSCC.RepeatingSectionItems[1].InsertItemAfter();
}
If you build and run this code as is then a System.Runtime.InteropServices.COMException is generated with text: "This method or property is not available because the current selection partially covers a plain text content control" on the statement that adds the Repeating Section control in the AddTableRepeatingSection() method (the line before InsertItemAfter).
However if you comment out the DocProtect() and DocUnprotect() statements in ThisDocument_StartUp then this code runs successfully.
What do I need to change to enable me to protect and unprotect the document without generating this exception when programmatically applying the repeating section content control?
I can duplicate what you're seeing - I don't know why it's doing this, seems to be almost some kind of race condition because after the document is opened (click "Continue") it works manually...
I found a workaround. It appears that selecting the table puts whatever is causing Word to pick up the content control in the first cell back where it belongs:
private void AddTableRepeatingSection()
{
//Programatically duplicates the table as a repeating section
Word.Table table = this.Sections[1].Range.Tables[1];
Word.Range rSRange = table.Range;
rSRange.Select();
Word.Range r = this.Application.Selection.Range;
Word.ContentControl rSCC = this.ContentControls.Add
(Word.WdContentControlType.wdContentControlRepeatingSection, r);
rSCC.RepeatingSectionItems[1].InsertItemAfter();
}

Replace text in Word with text from C# form

I'm trying to make an application in C#. When pressing a radio button, I'd like to open a Microsoft Word document (an invoice) and replace some text with text from my Form. The Word documents also contains some textboxes with text.
I've tried to implement the code written in this link Word Automation Find and Replace not including Text Boxes but when I press the radio button, a window appears asking for "the encoding that makes the document readable" and then the Word document opens and it's full of black triangles and other things instead of my initial template for the invoice.
How my invoice looks after:
Here is what I've tried:
string documentLocation = #"C:\\Documents\\Visual Studio 2015\\Project\\Invoice.doc";
private void yes_radioBtn_CheckedChanged(object sender, EventArgs e)
{
FindReplace(documentLocation, "HotelName", "MyHotelName");
Process process = new Process();
process.StartInfo.FileName = documentLocation;
process.Start();
}
private void FindReplace(string documentLocation, string findText, string replaceText)
{
var app = new Microsoft.Office.Interop.Word.Application();
var doc = app.Documents.Open(documentLocation);
var range = doc.Range();
range.Find.Execute(FindText: findText, Replace: WdReplace.wdReplaceAll, ReplaceWith: replaceText);
var shapes = doc.Shapes;
foreach (Shape shape in shapes)
{
var initialText = shape.TextFrame.TextRange.Text;
var resultingText = initialText.Replace(findText, replaceText);
shape.TextFrame.TextRange.Text = resultingText;
}
doc.Save();
doc.Close();
Marshal.ReleaseComObject(app);
}
So if your word template is the same each time you essentially
Copy The Template
Work On The Template
Save In Desired Format
Delete Template Copy
Each of the sections that you are replacing within your word document you have to insert a bookmark for that location (easiest way to input text in an area).
I always create a function to accomplish this, and I end up passing in the path - as well as all of the text to replace my in-document bookmarks. The function call can get long sometimes, but it works for me.
Application app = new Application();
Document doc = app.Documents.Open("sDocumentCopyPath.docx");
if (doc.Bookmarks.Exists("bookmark_1"))
{
object oBookMark = "bookmark_1";
doc.Bookmarks.get_Item(ref oBookMark).Range.Text = My Text To Replace bookmark_1;
}
if (doc.Bookmarks.Exists("bookmark_2"))
{
object oBookMark = "bookmark_2";
doc.Bookmarks.get_Item(ref oBookMark).Range.Text = My Text To Replace bookmark_2;
}
doc.ExportAsFixedFormat("myNewPdf.pdf", WdExportFormat.wdExportFormatPDF);
((_Document)doc).Close();
((_Application)app).Quit();
This code should get you up and running unless you want to pass in all the values into a function.
EDIT: If you need more examples I'm working on a blog post as well, so I have a lot more detail if this wasn't clear enough for your use case.

Outlook AddIn Zoom.Percentage

Im trying to translate this VBA code from an Outlook AddIn to C#
Private Sub objInspector_Activate() Handles objInspector.Activate
Dim wdDoc As Microsoft.Office.Interop.Word.Document = objInspector.WordEditor
wdDoc.Windows(1).Panes(1).View.Zoom.Percentage = lngZoom
End Sub
But I can't get access to the Panes.View.Zoom.Percentage property
The main idea is that when the user opens an email, he will get a custom zoom level.
What I got at the moment is:
void Inspector_Activate()
{
// this bool is true
// bool iswordMail = objInspector.IsWordMail();
//I get the word document
Document word = objInspector.WordEditor as Microsoft.Office.Interop.Word.Document;
word.Application.ActiveDocument.ActiveWindow.View.Zoom.Percentage = 150;
// at this point i'm getting an exception
// I've also tried with
// word.ActiveWindow.ActivePane.View.Zoom.Percentage = 150; getting the same exception
}
The exception is :
An exception of type 'System.Runtime.InteropServices.COMException'
occurred in OutlookAddInTest.dll but was not handled in user code
Additional information: This object model command is not available in
e-mail.
I'm quite new in C# and Office addins, any advise?
Use word.Windows.Item(1).View.Zoom.Percentage = 150 (where word comes from Inspector.WordEditor)
word.Application.ActiveDocument.ActiveWindow.View.Zoom.Percentage = 150;
What property exactly fires the exception?
Anyway, there is no need to call the Application and ActiveDocument properties in the code. The WordEditor property of the Inspector class returns an instance of the Document class (not Word Application instance).
Thanks to Eugene Astafiev for his help.
The square brackets did the trick
VBA
Private Sub objInspector_Activate() Handles objInspector.Activate
Dim wdDoc As Microsoft.Office.Interop.Word.Document = objInspector.WordEditor
wdDoc.Windows(1).Panes(1).View.Zoom.Percentage = 150
End Sub
C#
private void Inspector_Activate()
{
Document wdDoc = objInspector.WordEditor;
wdDoc.Windows[1].Panes[1].View.Zoom.Percentage = 150;
}
I've been wanting this forever, and then I stumbled on a nice project in the MSDN Gallery Outlook 2010: Developing an Inspector Wrapper. It has a set of wrappers for all the Outlook objects, so you get a true event for every item of interest. Not sure if it's the most efficient thing ever, but it seems to work.
I have trouble with my eyesight so want black everything, and zoom everything. I seem to be able to do that by overriding the Activate() method. It's all pretty new so we'll see if it survives long term.
protected virtual void Activate() {
var activeDocument = Inspector.WordEditor as Document;
if (activeDocument == null)
return;
var mailZoom = GetSetting("MailZoom", 125);
if (mailZoom != 0)
activeDocument.Windows[1].View.Zoom.Percentage = mailZoom;
if (GetSetting("MailBlack", true)) {
activeDocument.Background.Fill.ForeColor.RGB = 0;
activeDocument.Background.Fill.Visible = msoTrue;
activeDocument.Saved = true;
}
}
In this example, GetSetting is just a function that returns a setting from an INI file. you can use constants or some other storage method.
There might be a better way to get the white on black text, but this seems pretty good.

Change link without change the name

Is it possible to change link without changing the name?
ex:
linllabeltext.link = "http://mylink.com/";
doesn't work
and this change the name
linklabeltext.test = "http://mylink.com/"
change the test
I have added this function at click
Process.Start(linklabetext.text);
how?
full code:
private void (......)
{
.....
var name = result.name;
.......
labelLink1.text = name;
}
private void labelLink1_click....
{
Process.Start(labelLink1.text);
}
but this code change the name of labelLink1 in a link es: http://mysate.com but the name of labelLink is Visit a Web Site
Take a look at the examples on MSDN. Specifically where they create the LinkLabel and set it's link(s) and text:
this.linkLabel1 = new System.Windows.Forms.LinkLabel();
this.linkLabel1.Text = "Register Online. Visit Microsoft. Visit MSN.";
if(this.linkLabel1.Text.Length >= 45)
{
this.linkLabel1.Links[0].LinkData = "Register";
this.linkLabel1.Links.Add(24, 9, "www.microsoft.com");
this.linkLabel1.Links.Add(42, 3, "www.msn.com");
// The second link is disabled and will appear as red.
this.linkLabel1.Links[1].Enabled = false;
}
I've never actually used this control before, but it appears that you set the .Text to any string and then set the "links" to correspond to substrings within the .Text property.
Edit: I just noticed that you're also using the wrong event for clicking on the link. You don't want to bind to the LinkLabel control's Click event. It has a LinkClicked event which puts more information in the event about the link being clicked. Take a look at, of course, the MSDN examples:
private void linkLabel1_LinkClicked(object sender, System.Windows.Forms.LinkLabelLinkClickedEventArgs e)
{
// Specify that the link was visited.
this.linkLabel1.LinkVisited = true;
// Navigate to a URL.
System.Diagnostics.Process.Start("http://www.microsoft.com");
}
Dunno if it'll help or not, since I'm not completely sure what you're after, but here's a quick example of how to use a LinkLabel. Enter any valid url in the text box, click the link below it, and the url will be opened by calling Process.Start(). The text of the LinkLabel will not change, regardless of what url you enter. (Which I think is what you're after.)

Categories