C1RichTextBox with custom copy/paste behavior - c#

When using C1RichTextBox in Silverlight 5 with IE 10, I am facing two major issues:
During a clipboard paste operation, how can I detect if the content was copied from another C1RichTextBox in my Silverlight application or from an external application? From external applications, only text should be pasted without formatting.
Copy/Pasting large inline images from one C1RichTextBox to another does not work. The <img> elements have the image content stored in their data URL. If the image becomes too large (approx 1MB), the src attribute is dropped when copied to the clipboard.
The solution should:
Not have side-effects on the global clipboard or on the editing behavior of the C1RichTextBox.
Be robust against changes to the C1RichTextBox implementation.
Not have to modify/parse/analyze the HTML document in the clipboard.

It took me a while to figure all this (an a lot more...) out and I am happy to share with anyone who has to deal with these issues.
I am using a derived class to solve the issues
public class C1RichTextBoxExt : C1RichTextBox
{
1. Pasting from external application with text-only
The solution is simple in theory: Get a hold of the HTML after text from within the RichTextBox was copied/cut to the clipboard. When pasting, compare the current HTML in the clipboard with what was last copied. Because the clipboard in ComponentOne is global, the content changes if a Copy/Cut was done in another application and thus the HTML will be different.
To remember the last copied HTML, we use a static member inside C1RichTextBoxExt:
private static string _clipboardHtml;
The bad news is: The C1RichTextBox.ClipboardCopy() etc. methods are not virtual. The good news is: The keyboard shortcuts for Copy/Cut/Paste which call these methods can be disabled, e.g. in the constructor:
RemoveShortcut(ModifierKeys.Control, Key.C);
RemoveShortcut(ModifierKeys.Control, Key.Insert);
RemoveShortcut(ModifierKeys.Control, Key.V);
RemoveShortcut(ModifierKeys.Shift , Key.Insert);
RemoveShortcut(ModifierKeys.Control, Key.X);
RemoveShortcut(ModifierKeys.Shift , Key.Delete);
Now that the methods C1RichTextBox.ClipboardCopy() etc. are no longer called we can wire up our own version by overriding OnKeyDown:
protected override void OnKeyDown(KeyEventArgs e)
{
if ((Keyboard.Modifiers == ModifierKeys.Control) && (e.Key == Key.C)) { ClipboardCopy(); }
else if ((Keyboard.Modifiers == ModifierKeys.Control) && (e.Key == Key.Insert)) { ClipboardCopy(); }
else if ((Keyboard.Modifiers == ModifierKeys.Control) && (e.Key == Key.V)) { ClipboardPaste(); }
else if ((Keyboard.Modifiers == ModifierKeys.Control) && (e.Key == Key.X)) { ClipboardCut(); }
else if ((Keyboard.Modifiers == ModifierKeys.Shift) && (e.Key == Key.Insert)) { ClipboardPaste(); }
else if ((Keyboard.Modifiers == ModifierKeys.Shift) && (e.Key == Key.Delete)) { ClipboardCut(); }
else
{
// default behaviour
base.OnKeyDown(e);
return;
}
e.Handled = true; // base class should not fire KeyDown event
}
To not accidentally call the base class methods, I am overwriting them (see below, using new modifier). The ClipboardCopy() method just calls the base class and afterwards stores the clipboard HTML. A small pitfall here was to use Dispatcher.BeginInvoke() since the C1RichTextBox.ClipboardCopy() stores the selected text in the clipboard inside a Dispatcher.BeginInvoke() invocation. So the content will only be available after the dispatcher had a chance to run the action provided by C1RichTextBox.
new public void ClipboardCopy()
{
base.ClipboardCopy();
Dispatcher.BeginInvoke(() =>
{
_clipboardHtml = C1.Silverlight.Clipboard.GetHtmlData();
});
}
The ClipboardCut method is very similar:
new public void ClipboardCut()
{
base.ClipboardCut();
Dispatcher.BeginInvoke(() =>
{
_clipboardHtml = C1.Silverlight.Clipboard.GetHtmlData();
});
}
The ClipboardPaste method can now detect if pasting external data. Pasting text only is no so straightforward. I came up with the idea to replace the current clipboard content with the text-only representation of the clipboard. After pasting is done, the clipboard should be restored so the content can be pasted again in other applications. This also has to be done within a Dispatcher.BeginInvoke() since the base class method C1RichTextBox.ClipboardPaste() performs the paste operation in a delayed action as well.
new public void ClipboardPaste()
{
// If the text in the global clipboard matches the text stored in _clipboardText it is
// assumed that the HTML in the C1 clipboard is still valid
// (no other Copy was made by the user).
string current = C1.Silverlight.Clipboard.GetHtmlData();
if(current == _clipboardHtml)
{
// text is the same -> Let base class paste HTML
base.ClipboardPaste();
}
else
{
// let base class paste text only
string text = C1.Silverlight.Clipboard.GetTextData();
C1.Silverlight.Clipboard.SetData(text);
base.ClipboardPaste();
Dispatcher.BeginInvoke(() =>
{
// restore clipboard
C1.Silverlight.Clipboard.SetData(current);
});
}
}
2. Copy/Pasting large inline images
The idea here is similar: Remember the images when copied, put them back in during paste.
So first we need to store where which image is in the document:
private static List<C1TextElement> _clipboardImages;
private static int _imageCounter;
(The use of _imageCounter will be explained later...)
Then, before Copy/Cut is executed, we search for all images:
new public void ClipboardCopy()
{
_clipboardImages = FindImages(Selection);
base.ClipboardCopy();
// ... as posted above
}
and similar:
new public void ClipboardCut()
{
_clipboardImages = FindImages(Selection);
base.ClipboardCut();
// ... as posted above
}
The methods to find the images are:
private List<BitmapImage> FindImages(C1TextRange selection = null)
{
var result = new List<BitmapImage>();
if (selection == null)
{
// Document Contains all elements at the document level.
foreach (C1TextElement elem in Document)
{
FindImagesRecursive(elem, result);
}
}
else
{
// Selection contains all (selected) elements -> no need to search recursively
foreach (C1TextElement elem in selection.ContainedElements)
{
if (elem is C1InlineUIContainer)
{
FindImage(elem as C1InlineUIContainer, result);
}
}
}
return result;
}
private void FindImagesRecursive(C1TextElement elem, List<BitmapImage> list)
{
if (elem is C1Paragraph)
{
var para = (C1Paragraph)elem;
foreach (C1Inline inl in para.Inlines)
{
FindImagesRecursive(inl, list);
}
}
else if (elem is C1Span)
{
var span = (C1Span)elem;
foreach (C1Inline child in span.Inlines)
{
FindImagesRecursive(child, list);
}
}
else if (elem is C1InlineUIContainer)
{
FindImage(elem as C1InlineUIContainer, list);
}
}
private void FindImage(C1InlineUIContainer container, List<BitmapImage> list)
{
if (container.Content is BitmapImage)
{
list.Add(container.Content as BitmapImage);
}
}
I won't go into details about the above methods, they are pretty straightforward if you analyze the structure of C1RichTextBox.Document.
Now, how do we restore the images? The best I found is to use the ConvertingHtmlNode event of the C1RichTextBox.HtmlFilter. This event is fired every time a HTML node is converted into a C1TextElement. We subscribe to it in the constructor:
HtmlFilter.ConvertingHtmlNode += new EventHandler<ConvertingHtmlNodeEventArgs>(HtmlFilter_ConvertingHtmlNode);
and implement it like this:
void HtmlFilter_ConvertingHtmlNode(object sender, ConvertingHtmlNodeEventArgs e)
{
if (e.HtmlNode is C1HtmlElement)
{
var elem = e.HtmlNode as C1HtmlElement;
if (elem.Name.ToLower() == "img" && _clipboardImages != null && _clipboardImages.Count > _imageCounter)
{
if (!elem.Attributes.ContainsKey("src")) // key comparison is not case sensitive
{
e.Parent.Children.Add(_clipboardImages[_imageCounter].Clone());
e.Handled = true;
}
_imageCounter++;
}
}
}
So for each HTML element node with the name "img" we check if the "src" attribute is missing. If so, we add the next stored image instead and tell the event source that the event is now handled (for this HTML node) by setting e.Handled = true;
Which image is the "next" image is determined by the _imageCounter field which is incremented for each visited "img" element.
The _imageCounter field must be reset when ClipboardPaste() is invoked, so we do:
new public void ClipboardPaste()
{
_imageCounter = 0;
string current = C1.Silverlight.Clipboard.GetHtmlData();
// ... as posted above
}
Conclusion
If you copy/paste (no pun intended...) all code blocks posted above together, you should end up with a solution which has no side-effects (at least none known to the author as of today), is robust against changes and does almost no HTML processing.

Related

Is there a way how to step out form method and return page with updated element?

I have couple of method in code behind on asp.net application. I would like to return messages to user in Label in case of something happen during execution and stop execution.
Code is just as sample what I'm trying to achieve.
I already tried:
Response.Flush();
HttpContext.Current.ApplicationInstance.CompleteRequest();
Response.Redirect("to same page");
Example: (I don't want to execute unzipFile method. I would like to reload current page with updated label)
protected void btnUpload_Click(object sender, EventArgs e) {
uploadFile(Server.MapPath("~/") + filename);
unzipFile(string newFile);
}
protected void uploadFile(string newFile) {
if (newFile != null)
{
Label.Text="This is not valid file!"
//stop processing load file with updated label
}
if (newFile.ContentType != "application/x-zip-compressed") {
Label.Text="This is not valid file!"
//stop processing load file with updated label
}
}
Just don't redirect. The postback is already to the current page and should render the current page unless anything tells is not to. Consider a simple case:
protected void Page_Load(/.../)
{
// set some values on the page
}
protected void btnClick(/.../)
{
label1.Text = "This is a test";
}
With only that code in the button click handler, the current page will reload after clicking the button and the only visible change will be that text output.
Your case isn't really different. Set the labels and don't redirect. For example:
if (newFile != null)
{
Label.Text = "This is not valid file!";
}
else if (newFile.ContentType != "application/x-zip-compressed")
{
Label.Text = "This is not valid file!";
}
else
{
// process the file
Response.Redirect("SomewhereElse.aspx");
}
However you structure the logic, ultimately your goal is to not do any more processing once you have your error condition and just allow the event handler to complete and the page to re-render.
Note: I think you also meant == null, and you can simplify the conditions. Consider:
if (newFile == null || newFile.ContentType != "application/x-zip-compressed")
{
Label.Text = "This is not valid file!";
}
else
{
// process the file
Response.Redirect("SomewhereElse.aspx");
}
If you don't want the else, you can accomplish the same with a return:
if (newFile == null || newFile.ContentType != "application/x-zip-compressed")
{
Label.Text = "This is not valid file!";
return;
}
// process the file
Response.Redirect("SomewhereElse.aspx");

Navigation Helper SaveState / LoadState works incorrect

First, I read a list of entries from a database and display it in a ListView. When I leave the page to show details of an entry, then go back to the list, everything is ok.
Next, I open another page to add one entry to the database.
Go back to the list, reading from database shows me the correct count.
When I go to display one detail, the correct count is stored in SaveState.
Go back to the list, LoadState give the wrong count. It's the former state.
Display other details and go back now works with the old list and do not show me the added entry.
This is my code:
private void getList()
{
memoList = new List<MemoItem>();
db.loadHistory(ref memoList);
DelButton.IsEnabled = false;
System.Diagnostics.Debug.WriteLine("GetList, memoList[{0}]", memoList.Count);
}
private void NavigationHelper_SaveState(object sender, SaveStateEventArgs e)
{
if (memoList != null)
{
e.PageState["MemoItem"] = memoList;
if (memoSelected > -1)
e.PageState["memoSelected"] = memoSelected;
System.Diagnostics.Debug.WriteLine("SaveState, memoList[{0}]", memoList.Count);
}
}
private void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
if (e.PageState != null)
{
if (e.PageState.ContainsKey("MemoItem"))
{
memoList = (List<MemoItem>)e.PageState["MemoItem"];
System.Diagnostics.Debug.WriteLine("LoadState, memoList[{0}]", memoList.Count);
if (e.PageState.ContainsKey("memoSelected"))
memoSelected = (int)e.PageState["memoSelected"];
MemoListView.ItemsSource = memoList;
MemoListView.Visibility = Visibility.Visible;
}
else
{
getList();
showList();
}
}
}
Here are the Systems.Diagnostic outputs with comments in ():
GetList, memoList[8] (first time loaded)
SaveState, memoList[8] (leave the list to display details for one entry)
LoadState, memoList[8] (come back to the ListView)
SaveState, memoList[8] (leave the list to another page and add one entry)
GetList, memoList[9] (back to the list, read the correct entry count from database)
SaveState, memoList[9] (leave the ListView to display details for one entry)
LoadState, memoList[8] (come back to the ListView loads the wrong old list)
SaveState, memoList[8] (and works with the old list...)
LoadState, memoList[8] (...)
Remark: I can't call GetList from database every time, because I have to preserve checkmarks in the list which are not contained in the database.
What is wrong in my code? How can I resolve this problem? How to invalidate the StateEvent data after availability of a new list from database?
The problem is solved now. It was actually a consequence of the back stack manipulation.
I have four basic pages in my program, for which a GoBack makes sense. But I do not want to go back deeper. (I do not think anyone wants to go back 20 or more steps, but the seemingly unlimited BackStack would allow that.The required memory consumption is not negligible.)
In order to clear the BackStack at selecting one of the basic pages, I had this:
private void NavButton_Click(object sender, RoutedEventArgs e)
{
Button b = sender as Button;
if (b != null && b.Tag != null)
{
Type pageType = Type.GetType(b.Tag.ToString());
if (pageType != null)
{
if (rootFrame.CurrentSourcePageType != pageType)
{
rootFrame.Navigate(pageType, rootFrame);
// No goBack for Basic Pages
if (testBasicPage(pageType))
{
while (rootFrame.BackStackDepth > 1)
{
rootFrame.BackStack.RemoveAt(0);
}
}
}
}
}
}
That worked fine until I came across the problem with SaveState / Load State.
Now I first empty the BackStack and then navigate to the target page. That works.
private void NavButton_Click(object sender, RoutedEventArgs e)
{
Button b = sender as Button;
if (b != null && b.Tag != null)
{
Type pageType = Type.GetType(b.Tag.ToString());
if (pageType != null)
{
if (rootFrame.CurrentSourcePageType != pageType)
{
// No goBack for Basic Pages
if (testBasicPage(pageType))
{
while (rootFrame.BackStackDepth > 0)
{
rootFrame.BackStack.RemoveAt(0);
}
}
rootFrame.Navigate(pageType, rootFrame);
}
}
}
}

Calling object from mainform.cs in other forms?

i want to call anything from mainform (mainform.cs) from GraficDisplay (namespace)
in the other (namespace) : GraphLib , but i can't , would any one tell me why? and how can i resolve this problem? Its been giving me hard time since the start of the project, every time I try these errors appear:
When I call:
mainform.toolstriplabel1.text = "87";
this appears:
The name 'mainform' does not exist in the current context
and if I call this:
GraficDisplay.MainForm.toolstriplabel1.text = "87";
this appears:
The name 'GraficDisplay' does not exist in the current context
I mean I even can't call the GraficDisplay (namespace) in GraphLib (namespace)
also the MainForm is public and partial.
I usually don't follow links here either but CodeProject is a rather reliable source imo, so I had a look.
Edit: I was confused as to what you want. Here is what you seem to actually wnat:
The problem is about referencing a form or part of it from another form or part of it. It is further a problem of dealing with a Library, that really shouldn't be messed up be adding namespaces of an example application or dependencies etc..
So what you want is loose coupling.
Here is a solution that uses references in the library objects and register methods to fill the references. If you don't register anything the library will work normally.
This solution can be changed and expanded but I'll leave it at registering two objects: One is a Control, eg.g a TextBox; the other is a Component e.g. a ToolStripItem. If you want to reference only the ToolStripItem you can omit the references to the Control and the RegisterCtl methods.
In hat case you can and should also substitute 'Component' for 'ToolStripItem' to make things thighter!
First you go to the ultimate consumer of the actions, PlotterGraphSelectCurvesForm. Here you add these two blocks of code:
public partial class PlotterGraphSelectCurvesForm : Form
{
private int selectetGraphIndex = -1;
private PlotterGraphPaneEx gpane = null;
// block one: a Control reference (if you like!):
Control myTextCtl;
public void RegisterCtl(Control ctl) { if (ctl != null) myTextCtl = ctl; }
// block one: a Component reference:
Component myTextComp;
public void RegisterComp(Component comp) { if (comp != null) myTextComp = comp; }
//..
Next you code what you want to happen, maybe like this:
void tb_GraphName_TextChanged(object sender, EventArgs e)
{
if (selectetGraphIndex >= 0 && selectetGraphIndex < gpane.Sources.Count)
{
DataSource src = gpane.Sources[selectetGraphIndex];
String Text = tb_GraphName.Text;
// all controls have a Text:
if (myTextCtl != null) myTextCtl.Text = Text;
// here you need to know the type:
if (myTextComp != null) ((ToolStripItem) myTextComp).Text = Text;
//..
}
In theory all you now need to do is to register the TextBox and/or the ToolStripItem in the Mainform.. However, there is a complication: The PlotterGraphSelectCurvesForm is not called from the Mainform! Instead it is called from a UserObject PlotterGraphPaneEx, which in turn is sitting in the MainForm. In the same sprit of not messing up the library by creating dependencies you simply add the very same references and registration methods to this UO as well:
public partial class PlotterDisplayEx : UserControl
{
#region MEMBERS
Control myTextCtl;
public void RegisterCtl(Control ctl) { if (ctl != null) myTextCtl = ctl; }
Component myTextComp;
public void RegisterComp(Component comp) { if (comp != null) myTextComp = comp; }
//..
Now you can actually register things, both in the MainForm..:
public MainForm()
{
InitializeComponent();
display.RegisterCtl(aDemoTextBox);
display.RegisterComp(toolstriplabel1);
//..
..and in the UO:
private void selectGraphsToolStripMenuItem_Click(object sender, EventArgs e)
{
if (GraphPropertiesForm == null)
{
GraphPropertiesForm = new PlotterGraphSelectCurvesForm();
GraphPropertiesForm.RegisterCtl(myTextCtl);
GraphPropertiesForm.RegisterComp(myTextComp);
}
//..
Now when you open the Properties form and change the LabelText you can see both the text in the Graphs and the text in both the Menu and the TextBox change..

A Possible Threading/COM/UI problem

I am writing a toolbar for IE(6+). I have used the various sample bars from
codeproject.com (http://www.codeproject.com/KB/dotnet/IE_toolbar.aspx), and have a toolbar that works, registers unregisters etc. What I want the toolbar to do is to highlight divs within an html page as the users' mouse moves over that div. So far the highlighting code works, but I would like to display the name of the div (if it exists) in a label on the toolbar (changing as the mouse moves etc).
I cannot for the life of me get this to happen and trying to debug it is a nightmare. As the assembly is hosted in IE, I suspect that I am causing an exception (in IE) by trying to update the text on the label from a thread that didn't create that control, but because that exception is happening in IE, I don't see it.
Is the solution to try to update the control in a thread-safe way using Invoke? If so how?
Here is the event code:
private void Explorer_MouseOverEvent(mshtml.IHTMLEventObj e)
{
mshtml.IHTMLDocument2 doc = this.Explorer.Document as IHTMLDocument2;
element = doc.elementFromPoint(e.clientX, e.clientY);
if (element.tagName.Equals("DIV", StringComparison.InvariantCultureIgnoreCase))
{
element.style.border = "thin solid blue;";
if (element.className != null)
{
UpdateToolstrip(element.className);
}
}
e.returnValue = false;
}
and here is an attempt at thread-safe update of the toolbar:
delegate void UpdateToolstripDelegate(string text);
public void UpdateToolstrip(string text)
{
if (this.toolStripLabel1.InvokeRequired == false)
{
this.toolStripLabel1.Text = text;
}
else
{
this.Invoke(new UpdateToolstripDelegate(UpdateToolstrip), new object[] { text });
}
}
Any suggestions much appreciated.
I can't really reproduce the issue (creating a test project for an IE toolbar is a tad too much work), but you can try this:
Add the following routine to a public static (extensions methods) class:
public static void Invoke(this Control control, MethodInvoker methodInvoker)
{
if (control.InvokeRequired)
control.Invoke(methodInvoker);
else
methodInvoker();
}
And then replace the section of similar code in the first block with this:
if (element.className != null)
{
this.Invoke(() => toolStripLabel1.Text = element.className);
}
This is a sure-fire way of avoiding thread-safe issues in UI applications.

Google Suggestish text box (autocomplete)

What would be the best way to develop a text box that remembers the last x number of entries that were put into it. This is a standalone app written with C#.
This is actually fairly easy, especially in terms of showing the "AutoComplete" part of it. In terms of remembering the last x number of entries, you are just going to have to decide on a particular event (or events) that you consider as an entry being completed and write that entry off to a list... an AutoCompleteStringCollection to be precise.
The TextBox class has the 3 following properties that you will need:
AutoCompleteCustomSource
AutoCompleteMode
AutoCompleteSource
Set AutoCompleteMode to SuggestAppend and AutoCompleteSource to CustomSource.
Then at runtime, every time a new entry is made, use the Add() method of AutoCompleteStringCollection to add that entry to the list (and pop off any old ones if you want). You can actually do this operation directly on the AutoCompleteCustomSource property of the TextBox as long as you've already initialized it.
Now, every time you type in the TextBox it will suggest previous entries :)
See this article for a more complete example: http://www.c-sharpcorner.com/UploadFile/mahesh/AutoCompletion02012006113508AM/AutoCompletion.aspx
AutoComplete also has some built in features like FileSystem and URLs (though it only does stuff that was typed into IE...)
#Ethan
I forgot about the fact that you would want to save that so it wasn't a per session only thing :P But yes, you are completely correct.
This is easily done, especially since it's just basic strings, just write out the contents of AutoCompleteCustomSource from the TextBox to a text file, on separate lines.
I had a few minutes, so I wrote up a complete code example...I would've before as I always try to show code, but didn't have time. Anyway, here's the whole thing (minus the designer code).
namespace AutoComplete
{
public partial class Main : Form
{
//so you don't have to address "txtMain.AutoCompleteCustomSource" every time
AutoCompleteStringCollection acsc;
public Main()
{
InitializeComponent();
//Set to use a Custom source
txtMain.AutoCompleteSource = AutoCompleteSource.CustomSource;
//Set to show drop down *and* append current suggestion to end
txtMain.AutoCompleteMode = AutoCompleteMode.SuggestAppend;
//Init string collection.
acsc = new AutoCompleteStringCollection();
//Set txtMain's AutoComplete Source to acsc
txtMain.AutoCompleteCustomSource = acsc;
}
private void txtMain_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
//Only keep 10 AutoComplete strings
if (acsc.Count < 10)
{
//Add to collection
acsc.Add(txtMain.Text);
}
else
{
//remove oldest
acsc.RemoveAt(0);
//Add to collection
acsc.Add(txtMain.Text);
}
}
}
private void Main_FormClosed(object sender, FormClosedEventArgs e)
{
//open stream to AutoComplete save file
StreamWriter sw = new StreamWriter("AutoComplete.acs");
//Write AutoCompleteStringCollection to stream
foreach (string s in acsc)
sw.WriteLine(s);
//Flush to file
sw.Flush();
//Clean up
sw.Close();
sw.Dispose();
}
private void Main_Load(object sender, EventArgs e)
{
//open stream to AutoComplete save file
StreamReader sr = new StreamReader("AutoComplete.acs");
//initial read
string line = sr.ReadLine();
//loop until end
while (line != null)
{
//add to AutoCompleteStringCollection
acsc.Add(line);
//read again
line = sr.ReadLine();
}
//Clean up
sr.Close();
sr.Dispose();
}
}
}
This code will work exactly as is, you just need to create the GUI with a TextBox named txtMain and hook up the KeyDown, Closed and Load events to the TextBox and Main form.
Also note that, for this example and to make it simple, I just chose to detect the Enter key being pressed as my trigger to save the string to the collection. There is probably more/different events that would be better, depending on your needs.
Also, the model used for populating the collection is not very "smart." It simply deletes the oldest string when the collection gets to the limit of 10. This is likely not ideal, but works for the example. You would probably want some sort of rating system (especially if you really want it to be Google-ish)
A final note, the suggestions will actually show up in the order they are in the collection. If for some reason you want them to show up differently, just sort the list however you like.
Hope that helps!
I store the completion list in the registry.
The code I use is below. It's reusable, in three steps:
replace the namespace and classname in this code with whatever you use.
Call the FillFormFromRegistry() on the Form's Load event, and call SaveFormToRegistry on the Closing event.
compile this into your project.
You need to decorate the assembly with two attributes: [assembly: AssemblyProduct("...")] and [assembly: AssemblyCompany("...")] . (These attributes are normally set automatically in projects created within Visual Studio, so I don't count this as a step.)
Managing state this way is totally automatic and transparent to the user.
You can use the same pattern to store any sort of state for your WPF or WinForms app. Like state of textboxes, checkboxes, dropdowns. Also you can store/restore the size of the window - really handy - the next time the user runs the app, it opens in the same place, and with the same size, as when they closed it. You can store the number of times an app has been run. Lots of possibilities.
namespace Ionic.ExampleCode
{
public partial class NameOfYourForm
{
private void SaveFormToRegistry()
{
if (AppCuKey != null)
{
// the completion list
var converted = _completions.ToList().ConvertAll(x => x.XmlEscapeIexcl());
string completionString = String.Join("¡", converted.ToArray());
AppCuKey.SetValue(_rvn_Completions, completionString);
}
}
private void FillFormFromRegistry()
{
if (!stateLoaded)
{
if (AppCuKey != null)
{
// get the MRU list of .... whatever
_completions = new System.Windows.Forms.AutoCompleteStringCollection();
string c = (string)AppCuKey.GetValue(_rvn_Completions, "");
if (!String.IsNullOrEmpty(c))
{
string[] items = c.Split('¡');
if (items != null && items.Length > 0)
{
//_completions.AddRange(items);
foreach (string item in items)
_completions.Add(item.XmlUnescapeIexcl());
}
}
// Can also store/retrieve items in the registry for
// - textbox contents
// - checkbox state
// - splitter state
// - and so on
//
stateLoaded = true;
}
}
}
private Microsoft.Win32.RegistryKey AppCuKey
{
get
{
if (_appCuKey == null)
{
_appCuKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(AppRegistryPath, true);
if (_appCuKey == null)
_appCuKey = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(AppRegistryPath);
}
return _appCuKey;
}
set { _appCuKey = null; }
}
private string _appRegistryPath;
private string AppRegistryPath
{
get
{
if (_appRegistryPath == null)
{
// Use a registry path that depends on the assembly attributes,
// that are presumed to be elsewhere. Example:
//
// [assembly: AssemblyCompany("Dino Chiesa")]
// [assembly: AssemblyProduct("XPathVisualizer")]
var a = System.Reflection.Assembly.GetExecutingAssembly();
object[] attr = a.GetCustomAttributes(typeof(System.Reflection.AssemblyProductAttribute), true);
var p = attr[0] as System.Reflection.AssemblyProductAttribute;
attr = a.GetCustomAttributes(typeof(System.Reflection.AssemblyCompanyAttribute), true);
var c = attr[0] as System.Reflection.AssemblyCompanyAttribute;
_appRegistryPath = String.Format("Software\\{0}\\{1}",
p.Product, c.Company);
}
return _appRegistryPath;
}
}
private Microsoft.Win32.RegistryKey _appCuKey;
private string _rvn_Completions = "Completions";
private readonly int _MaxMruListSize = 14;
private System.Windows.Forms.AutoCompleteStringCollection _completions;
private bool stateLoaded;
}
public static class Extensions
{
public static string XmlEscapeIexcl(this String s)
{
while (s.Contains("¡"))
{
s = s.Replace("¡", "¡");
}
return s;
}
public static string XmlUnescapeIexcl(this String s)
{
while (s.Contains("¡"))
{
s = s.Replace("¡", "¡");
}
return s;
}
public static List<String> ToList(this System.Windows.Forms.AutoCompleteStringCollection coll)
{
var list = new List<String>();
foreach (string item in coll)
{
list.Add(item);
}
return list;
}
}
}
Some people shy away from using the Registry for storing state, but I find it's really easy and convenient. If you like, You can very easily build an installer that removes all the registry keys on uninstall.

Categories

Resources