Undo all actions of a button VSTO - c#

I am developing a MS Word Add-In, so I added a button that fires a function :
private void button1_Click(object sender, RibbonControlEventArgs e) {
// do some actions on a word document (text - formatting - ...) here.
}
My issue is, when the funcation performs n actions on the word document, I have to click undo n times to undo all button actions. This is a bad user experience to undo like 10 or 100 times to return to original state (text - formatting - ... etc).
Is there some way to pack all button actions as one action in the undo stack, so that I can undo button effect with a single click or Ctrl + z ?
Important note:
An alternative approach that does the job for me is to:
Open temp document.
Copy original document to temp document.
Do all edits on temp doc ( n actions taken on it).
Copy temp doc back to original doc (only one action taken which is paste, so I can undo it).
This is why I struggled in the second approach: check here

You could wrap everything in a custom undo-record - that way you only have to undo once to undo it all.
Visual Basic:
Application.UndoRecord.StartCustomRecord "Title of undo-record here"
Application.ScreenUpdating = False ' Optional, reduces screen flicker during operations.
'--- Your code here
Application.ScreenUpdating = True ' Optional, but required if set to false above
Application.UndoRecord.EndCustomRecord
' After this, you can undo once to undo it all.
C#:
Application.UndoRecord.StartCustomRecord "Title of undo-record here";
Application.ScreenUpdating = false; // Optional, reduces screen flicker during operations.
//--- Your code here
Application.ScreenUpdating = true; // Optional, but required if set to false above
Application.UndoRecord.EndCustomRecord;
// After this, you can undo once to undo it all.

This StackOverFlow Answer should point you in the right direction.
Save undo stack during macro run

Related

VSTO Word post save event (with information of Cancel Save)

I know there is already a post with the same name as this but it provides a partial solution for the problem. The post is: VSTO Word post save event
I'm using this class and it helped me a lot. However, when I make changes in the Word file and try to Close the application clicking in the option "Not Save" the event of save is raised.
How can I know if the user has clicked in the "Save" or "Not Save" buttom when trying to close the window? I've tried everything but I can't know this information.
A quick test in VBA makes me believe this approach would be promising. DocumentBeforeClose is triggered before DocumentBeforeSave.
Declare a class-level field (saveStatus in the below code snippet).
In DocumentBeforeClose set it to "False" on the assumption the user won't save. If the user does save, set the value to True in DocumentBeforeSave. If you need to do something with the document when it's saved, put that code in this event, as well.
Private saveStatus as Boolean
Private Sub app_DocumentBeforeClose(ByVal Doc As Document, Cancel As Boolean)
saveStatus = False
Debug.Print saveStatus
End Sub
Private Sub app_DocumentBeforeSave(ByVal Doc As Document, SaveAsUI As Boolean, Cancel As Boolean)
saveStatus = True
Debug.Print saveStatus
End Sub
For those facing the same problem I found a solution. Before I open the file in Word I read all bytes and store in a class variable, for example, wordContent using
wordContent = System.IO.File.ReadAllBytes(path);
Then, every time that I save without closing the application (i.e clicking in the save buttom in Word) I update this variable wordContent.
So, when I close the application and the event AfterSave is fired with my variable isClosed == true, in this point I don't know if the user closed the application clicking in Save or Not Save. So, I read the bytes of the word File and compare with my wordContent. When the user has clicked in the "Save" option the contents will be different, and when the user has clicked in the "Not Save" options the contents will be equal.
So, whatever it needs to be done it'll be in the comparison of this two byte arrays.
Remembering that I'm using the WordSaveHandler provided by the post that I pointed in the question above, this class can handle if the users save by clicking in the Save Buttom or by closing the application.

VSTO Word 2016: Squiggly underline without affecting undo

I am working on a real-time language analysis tool that needs to highlight words to draw attention from the writer in Word 2016 using a VSTO add-in, written in .NET4.6.1 with C#. Think of the grammar/spelling check, which adds a squiggly line underneath a word to show you that the word has grammatical or spelling errors. I'm adding a similar feature for some of my own defined rules.
I searched around on adding squiggly lines, and stumbled on Font.Underline and Font.UnderlineColor. I set this on the range of a word, and it appears to provided that visual stumili I was after to draw attention. There is a problem, though. Every underline I add or underline color I change adds an undo action to the undo stack.
I don't want this to happen, or I want a way to pop the action I just did in code from the stack. The aim is to have the user be able to use CTRL+Z to remove text he changed, and not affect my language anlysis result.
How would I go about doing this?
I stumbled on this question, which is asking exactly the same thing:
Prevent actions to be added to the word undo redo OR Remove actions from the undo redo CommandBarComboBox
As #Manu pointed out that same question was also asked over at MSDN where the answer was:
The UndoClear method will empty the list. That's the best you can do.
There is also a similar question on here Word VSTO override CTRL+Z / CTRL+Y
which suggests the Microsoft.Office.Interop.Word.UndoRecord route or MessageHooks in AddIns.
After more research I noticed a great idea at the end of this thread: MSDN Draw my own squigglies on Word document where you keep track of your actions and then skip past them in Undo and Redo operations.
Here is an excellent example of code to do "transactional undo/redo's" Can I create an undo transaction in Word or Excel? (VSTO) . You can do this same method in VSTO except for one big problem, as noted by Dirk Vollmar in his answer:
I don't think that overwriting built-in Word commands is possible using VSTO alone, though
I have overwritten some built-in commands in VSTO using keyboard hooking events to intercept commands: How to perform .Onkey Event in an Excel Add-In created with Visual Studio 2010?
However I'm not sure if you can recreate the Ribbon to intercept button commands. More specifically, the Undo and Redo are built-in galleries in the Ribbon UI and you can do nothing with a built-in Ribbon gallery. And in versions like 2010 the Undo/Redo buttons are in the title bar - and you cannot add/edit buttons on the title bar using VSTO:
So if you're concerned with trapping the button commands (everyone I know uses Ctrl+Z & Y), you might inject VBA code to get access to EditUndo and EditRedo events, eg:
VB._VBComponent vbModule = VBProj.VBE.ActiveVBProject.VBComponents.Add(VB.vbext_ComponentType.vbext_ct_StdModule);
String functionText = "Public Sub EditUndo() \n";
functionText += "MsgBox \"Undo Happened\"\n";
functionText += "End Sub";
vbModule.CodeModule.AddFromString(functionText);
Main problem with this approach is Trust needs to be granted.
Another answer in this same QA
Can I create an undo transaction in Word or Excel? (VSTO) is by Mike Regan who answered 3 months after Dirk. He used a hidden document and placed it in the real document when needed to make any amount of VSTO actions a single undo.
Still doesn't solve the problem of preventing an action being recorded in the Undo History.
I did try a Registry key to limit the UndoHistory to 0 and reset it back to 100 (in order to disable the History while adding an action), but it appears its only for Excel
https://support.microsoft.com/en-gb/kb/211922
There maybe an undocumented reg key to disable the Undo/Redo history altogether but it would only be read by Word on startup. I thought the UndoHistory key containing a number would be read before each Undo/Redo, but no luck with this approach at all.
It's not an easy problem to solve there are big limitations, so it might be easier to:
a) Accept that your spell/grammer checker Add-In is included in the Undo/Redo list (defeat).
b) Work out where the line/text is on screen and show a transparent tooltip highlighting the problem. This is a lot harder than it seems and is less than ideal, here are two great answers to guide you on this method: Detecting text changes in Word 2016 from VSTO add-in or a much simpler approach to detect XY positions from this Microsoft email thread: https://groups.google.com/forum/#!topic/microsoft.public.word.vba.general/pKq4PsqD3cM
//position dialog relative to word insertion point (caret)
int left = 0;
int top = 0;
int width = 0;
int height = 0;
MSWord.Range r = Globals.ThisDocument.Application.Selection.Range;
MSWord.Window w = Globals.ThisDocument.ActiveWindow;
w.GetPoint(out left, out top, out width, out height, r);
frmPopUp newForm = new frmPopUp();
newForm.SetDesktopLocation( left + width + 2, top - newForm.Height + height );
c) Only trap Undo/Redo events by the Keyboard with Transactional Undo/Redo's and let users see the atomic Undo/Redo's using the buttons. It would be extremely dodgy to remove the Undo/Redo buttons and that will cause heaps of Where's my Undo button gone? support cases. So don't do this, if you're curious, quick access toolobar customization can be done from ".qat" files. Ref: https://support.microsoft.com/en-us/kb/926805
d) Use a Mouse Hook and detect when the Undo/Redo buttons are clicked. Here is the code to hook up the mouse, note this wont play nice with corporate Anti-Virus products and I dont recommend it: https://blogs.msdn.microsoft.com/andreww/2009/02/24/message-hooks-in-add-ins/ or https://github.com/gmamaladze/globalmousekeyhook.
e) Try to intercept raw windows messages, eg Excel CustomTaskPane with WebBrowser control - keyboard/focus issues just beware as per my comment referencing BUG: Cant choose dates on a DatePicker that fall outside a floating VSTO Add-In that message pumps in Office sometimes exhibit weird behaviour.
Unfortunately, it doesn't look like there is an easy solution to this problem. From what I've seen, you have two options:
You could clear the entire undo stack using Document.UndoClear. However, this won't be very user friendly, since all previous actions aren't in the undo list anymore.
Work with the UndoRecord class. Between your calls to StartCustomRecord and EndCustomRecord, all actions in your code will only generate one item in the undo stack. CTRL+Z will still affect your language analysis, but the whole undo stack won't be as polluted as before.
I know this isn't what you want, but even Microsoft MVPs don't have a better solution.

Concurrency and multiple user access

First of all I would explain the situation.
I have a GUI which is stored on a server and should gain multiple user access. Users are working on different tabs (kind of separated workspaces).
Now when a user enters a tab it should be locked for other users (disabled).Other users should see if a tab is currently locked.
So my ideas on this are:
Create a textfile which lists the tabs and their current status (locked-unlocked).
A thread checks every few seconds if that file was modified. If yes then read it and disable or enable the corresponding tabs in the GUI.
3.When a user wants to enter a tab, check the file if the tab is unlocked (maybe a user was faster than the thread) and set it to locked.
When a user leaves a tab then set it to unlocked.
Now my questions are:
Is there a better or more efficient way to handle this situation?
And how to avoid the case that two users want to switch to the same tab at the same time, read that the tab is unlocked and write at the same time into the file?
Are there even more error cases?
Instead of a text file, you could use a proper SQL DBMS and transactions to keep the checking and updating atomic. It will cover all edge cases if you write your queries properly.
Something like:
begin tran
if not exists(select top 1 1 from tabs where id=#id and active=1)
begin
update tabs set active=1 where id=#id
select 1 --result
end
else
begin
select 0 --result
end
commit tran
Or even better, instead of just storing true/false to mean that the tab is in use, store a user identifier to know who's using it. That way if your application crashes/ends before releasing it, you can manually release it next time the same user logs in. You can also throw in an expiration date for the same purpose, and keep pushing it back while the application is online.
I understand from your question that you need to synchronize the applications running on different computers. You want to do it through a file located on a network drive, that is accessed from all of these computers, right?
Except shared file you can use IP/Ethernet broadcasting, or simple synchronizing server.
Among all these solutions the shared file is most simple, but no so effective.
Instead of it you can use similar technique with System.IO.FileSystemWatcher. In case of file watcher you'll create single file for each tab while it's locking; and delete it while tab is releasing.
File watcher frees you from having to re-read the content of the file, it raises an event instead when the file is changed.
This method would work with no so high load. If you're planning >50 users I think it's better to develop special server to synchronize users.
Short instructions:
Place FileSystemWatcher control on a form.
Setup property Path to desired network path (it's better to use new empty directory).
Create handlers for event Create and Delete:
void fileSystemWatcher1_Created(object sender, FileSystemEventArgs e)
{
var tabName = Path.GetFileName(e.FullPath);
var tab = tabControl1.TabPages[tabName];
// Do not disable selected tab cause you're working with the tab
// and you're locking it
if (tab != tabControl1.SelectedTab)
tab.Enabled = false;
}
void fileSystemWatcher1_Deleted(object sender, FileSystemEventArgs e)
{
var tabName = Path.GetFileName(e.FullPath);
var tab = tabControl1.TabPages[tabName];
tab.Enabled = true;
}
When a tab gets focus, you should create empty file with the tab name in the network directory:
. . .
var path = fileSystemWatcher1.Path;
var filename = Path.Combine(path, tabControl1.SelectedTag.Name);
using (File.Create(filename));
When a tab losts focus: you should delete file:
var path = fileSystemWatcher1.Path;
var filename = Path.Combine(path, tabControl1.SelectedTag.Name);
File.Delete(filename);
I feel xml file will be better than text file for this. As specified in the question you need to maintain status of each tab in the xml file indicating whether it is locked. you can handle selecting event of the tab control in which the type parameter e is TabControlCancelEventArgs and has the property Cancel by setting which to true will cancel selecting that tab. within this event you can check the xml file to verify that the tab is locked or unlocked and if locked, you can set e.Cancel=true and give a message to the user that this tab is locked.
you can use deselecting event of the tab control to know when a user moves out of that tab and update the status of that tab in xml file to unlocked.

Word: SyncScrollingSideBySide and ScrollIntoView

One feature of our Word add-in shows two document windows side-by-side. The user can double-click a paragraph in the left-hand document to scroll an associated paragraph in the right-hand document into view. When we do this, we want to re-enable Synchronous Scrolling if it was enabled before the double-click. We're doing something like this:
private void LineUpParagraphs()
{
// Unlock the views so we can scroll them independently.
bool wasSyncEnabled = this.originalDocument.Document.Windows.SyncScrollingSideBySide;
this.originalDocument.Document.Windows.SyncScrollingSideBySide = false;
// Scroll corresponding original paragraph into view.
this.originalDocument.Document.Windows[1].ScrollIntoView(
this.CurrentOriginalParagraph.Range);
// Re-enable synchronous scrolling if it was enabled before.
if (wasSyncEnabled)
{
this.originalDocument.Document.Windows.SyncScrollingSideBySide = true;
}
}
After doing this, the desired range is in view in the original (right-hand for our app) document, but as soon as you scroll either window, the right-hand window jumps back to its original position.
Things we've tried that didn't work:
Set the SyncScrollingSideBySide property on all of the Application windows rather than just one of the two compare documents.
Toggle the property an additional time.
We've resorted to SendKeys to simulate a click on the Synchronous Scrolling button. (If you don't re-enable the sync programatically, then click the button yourself, the right-hand document doesn't jump back to its original position when you scroll). This isn't really an acceptable solution, though--it is inconsistent for example depending on whether our add-in's tab is active. Sometimes it works, sometimes it toggles the sync scrolling an additional time which will annoy the customer. Is there a better way?
Note: The issue occurs when the left-hand document is longer than the right-hand document (the one being scrolled).

How to detect previous state of text field in C#?

I'm coding a simple text editor using Windows Forms. As in many editors, when the text changes the title bar displays an asterisk next to the title, showing that there is unsaved work. When the user saves, this goes away.
However, there is a problem. This is handled in the change event of the main text box. But this gets called too when a file is opened or the user selects "New file", so that if you open the editor and then open a file, the program says that there are unsaved changes. What is a possible solution?
I thought of having a global variable that says whether the text changed in a way that shouldn't trigger the asterisk, but there has to be a better way.
before loading data to a textbox, unassociate first the eventhandler for change
uxName.TextChanged -= uxName_TextChanged;
uxName.Text = File.ReadAllText("something.txt");
uxName.TextChanged += uxName_TextChanged;
This is a horrible solution, but every time the text change event fires, compare the value of the textbox to some variable, and if they are different store the contents on the textbox in a variable and add the asterisk. When the method is invoked via the New File dialog or any other such event that is NOT changing the text, the asterisk won't appear.
This is not a viable solution for a real text editor since the memory would quickly get out of hand on even medium-sized files. Using a finger tree or whatever data structure text editors use to compare "versions" of the text is the only real efficient solution, but the premise is the same.
http://scienceblogs.com/goodmath/2009/05/finally_finger_trees.php
Below the second picture he mentions the use of finger trees in text editors to implement an extremely cheap "undo" feature, but I'm sure you can see the validity of the tree for your problem as well.
There are no global variables in C#. You should have such an variable as an instance variable in your form (or better yet, in a model for which your form is a view), and that is perfectly fine.
This is a very simple and stupid solution. I would use a MVP design pattern for this but here the fastest and simple solution:
//Declare a flag to block the processing of your event
private bool isEventBlocked = false;
private void OnTextChanged(object sender, EventArgs e)
{
if(!isEventBlocked)
{
//do your stuff
}
}
private void OnNewFile() //OR OnOpenFile()
{
try
{
isEventBlocked = true;
CreateFile();
}
catch
{
//manage exception
}
finally
{
isEventBlocked = false;
}
}

Categories