|
luglio 2006 Blog Posts
Qualche ora fa è stata resa disponibile la prima CTP di Sandcastle. Per il download diretto, fate clic sul seguente collegamento: http://www.microsoft.com/downloads/details.aspx?FamilyId=E82EA71D-DA89-42EE-A715-696E3A4873B2&displaylang=en. Per maggiori informazioni, invece, visitate questa pagina, che tra le altre cose fornisce un esempio passo-passo che spiega come realizzare un file CHM con Sandcastle.
Segnalo un altro articolo di MSDN Magazine che ritengo particolarmente interessante: ASP.NET Atlas Powers the AJAX-Style Sites You’ve Been Waiting For. Come si intuisce dal nome, esso fornisce un'introduzione ad Atlas, l'implementazione del pattern AJAX secondo Microsoft:
Atlas, an extension of the .NET Framework 2.0, enables developers to more easily create rich, interactive Web sites that take advantage of both browser and server features.
powered by IMHO 1.3
Questo articolo: http://msdn.microsoft.com/msdnmag/issues/06/07/AdvancedBasics/, preso da MSDN Magazine di Luglio, spiega come utilizzare gli oggetti messi a disposizione dal namespace System.Diagnostics per monitorare la propria applicazione, ad esempio definendo un Performance Counter oppure scrivendo nel Log di sistema. Gli esempi di codice sono scritti in VB .NET 2005, per il quale vengono anche mostrate le scorciatoie utilizzando il namespace My, ma quanto detto si può realizzare facilmente anche con C#.
Può sembrare un task banale, invece, se si deve cambiare a runtime la stringa di connessione di un report realizzato con Crystal Reports, è necessario scrivere alcune righe di codice. Sul sito di Code Project, all'indirizzo http://www.codeproject.com/useritems/Crystal_Report_Connection.asp, è illustrata una procedura che spiega come fare. Non è niente di eccezionale, ma è comunque uno snippet di codice abbastanza utile.
Al momento questa pagina risulta inaccessibile, ma per fortuna ci viene in aiuto la cara funzione cache di Google. Qui di seguito riporto il codice contenuto nella suddetta pagina:
'' this project creates the crystal report using the ADO.net features'1. ADD form'2. Add Crystalviewer on the form'3. Add dataset from Add new File'4. Add new element on to the Dataset (the element name should be the same as the name of the field or column 'in the current table )'5. Add Crytal report from Add new file'6. Follow the wizard to create connection for the report, Select the project Data and select the Dataset that u create in the project'7. Select the fields u want to display ' BEST PART IS THAT U CAN CHANGE THE DATABSE CONNECTION AT RUNTIME ' BUT IT SHOULD BE THE SAME DATABSE AND SHOULD HAVE SAME TABLE ' THIS HELPS U WHEN U INSTALL UR APPLICATION ONTO THE USER MACHINE WHERE THE DIRECTORY STRUCTURE WOULD NOT ' BE THE SAME' TO DO THAT U NEED TO CHANGE THE DATASOURCE PATH NAME '' THIS WORKS WITH ALL KIND THE DATABASE '' ALSO MYSQL'' IF U WANT TO CHANGE THE DATABSE OTHER THAN I USED (MICROSOFT ACCES) U NEED TO CHANGE THE CONNECTION STRING '' THAT IS THE DRIVER DETAILS AND MAY BE THE FORMAT APPROVED BY THAT DATABSE'' TO GET THE DIFFRENT DATABSE CONNECTION STRING GOTO http://www.connectionstrings.com/'' THE CURRENT STRING IS DSNLess STRING AND IT TO THE SAMPLE DATABASE OF CRYSTAL REPORT IN VISUAL STUDIO 2003'' DATABASE NAME = XTREME.MDB Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load Dim con As New OdbcConnection Dim com As New OdbcCommand Dim adp As New OdbcDataAdapter Dim ds As New Dataset1 ds.Clear() Dim path = Application.StartupPath.ToString con.ConnectionString = "Driver={Microsoft Access Driver (*.mdb)};Dbq= C: Files Visual Studio .NET 2003 Reports.mdb;Uid=Admin;Pwd=;" com.CommandText = "select EmployeeID, [Last Name], FirstName from employee" com.CommandType = CommandType.Text com.Connection = con adp.SelectCommand = com Try adp.Fill(ds, "employeeDS") Catch ex As Odbc.OdbcException MsgBox(ex.Message) End Try Try Dim myreport As New CrystalReport1 myreport.SetDataSource(ds) cr.ReportSource = myreport Catch ex As Exception MsgBox(ex.Message) End TryEnd Sub
powered by IMHO 1.3
Leggendo su una rivisita di informatica, sono venuto a conoscenza di Media Portal, un programma Open Source che permette di realizzare un sistema in stile Windows Media Center, ricalcandone anche l'interfaccia, a costo 0. Lo sto provando proprio in questo momento: pur non disponendo di tutte le funzionalità del suo "fratello maggiore", mi sembra veramente ottimo, considerando anche il prezzo a cui viene offerto... Per quanti fossero interessati, l'indirizzo è http://www.team-mediaportal.com/.
Spesso si ha la necessità di aggiungere al menu
File di un'applicazione la classica funzionalità Documenti
recenti a cui ormai tutti siamo abituati. Con il seguente snippet di codice
è possibile semplificare enormemente questo task:
using System;using System.Collections.Specialized;using System.Text;using System.Windows.Forms;using System.IO;using System.Collections;using System.Runtime.InteropServices;namespace SpecialServices{ public delegate void RecentFileSelectedEventHandler(RecentFiles sender, string FileName); public class RecentFiles { #region Platform Invoke private const int SHARD_PATH = 0x2; [DllImport("shell32.dll")] private static extern void SHAddToRecentDocs(uint uFlags, string pv); #endregion private const int MAX_MENU_ITEMS = 4; private const int MAX_LENGTH = 24; private ToolStripMenuItem mMenu; private int mPosition; private int mMaxMenuItems; private StringCollection mFiles; private RecentFileSelectedEventHandler mOnClick; private bool mShowRecentFiles; public RecentFiles(ToolStripMenuItem Menu, RecentFileSelectedEventHandler onClick) : this(Menu, Menu.DropDownItems.Count, MAX_MENU_ITEMS, onClick) { } public RecentFiles(ToolStripMenuItem Menu, int Position, RecentFileSelectedEventHandler onClick) : this(Menu, Position, MAX_MENU_ITEMS, onClick) {} public RecentFiles(ToolStripMenuItem Menu, int Position, int MaxMenuItems, RecentFileSelectedEventHandler onClick) : this(Menu, Position, MaxMenuItems, true, onClick) { } public RecentFiles(ToolStripMenuItem Menu, int Position, int MaxMenuItems, bool ShowItem, RecentFileSelectedEventHandler onClick) { mFiles = Settings.Default.RecentFiles; if (mFiles == null) mFiles = new StringCollection(); mMenu = Menu; mPosition = Position; mMaxMenuItems = MaxMenuItems; mOnClick = onClick; mShowRecentFiles = ShowItem; if (mShowRecentFiles) { //Visualizza l'elenco dei file recenti. this.DrawMenu(); } } #region ShortenPathName private string ShortenPathName(string PathName) { if (PathName.Length <= MAX_LENGTH) return PathName; StringBuilder root = new StringBuilder(Path.GetPathRoot(PathName)); if (root.Length > 3) root.Append(Path.DirectorySeparatorChar); String[] elements = PathName.Substring(root.Length).Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); int filenameIndex = elements.GetLength(0) - 1; if (elements.GetLength(0) == 1) { if (elements[0].Length > 5) { if (root.Length + 6 >= MAX_LENGTH) return root.Append(elements[0].Substring(0, 3) + "...").ToString(); else return PathName.Substring(0, MAX_LENGTH - 3) + "..."; } } else if ((root.Length + 4 + elements[filenameIndex].Length) > MAX_LENGTH) { root.Append("...\\"); int len = elements[filenameIndex].Length; if (len < 6) return root.Append(elements[filenameIndex]).ToString(); if ((root.Length + 6) >= MAX_LENGTH) len = 3; else len = MAX_LENGTH - root.Length - 3; return root.Append(elements[filenameIndex].Substring(0, len) + "...").ToString(); } else if (elements.GetLength(0) == 2) { return root.Append("...\\" + elements[1]).ToString(); } else { int len = 0; int begin = 0; for (int i = 0; i < filenameIndex; i++) { if (elements[i].Length > len) { begin = i; len = elements[i].Length; } } int totalLength = PathName.Length - len + 3; int end = begin + 1; while (totalLength > MAX_LENGTH) { if (begin > 0) totalLength -= elements[--begin].Length - 1; if (totalLength <= MAX_LENGTH) break; if (end < filenameIndex) totalLength -= elements[++end].Length - 1; if (begin == 0 && end == filenameIndex) break; } for (int i = 0; i < begin; i++) { root.Append(elements[i]); root.Append('\\'); } root.Append("...\\"); for (int i = end; i < filenameIndex; i++) { root.Append(elements[i]); root.Append('\\'); } return root.Append(elements[filenameIndex]).ToString(); } return PathName; } #endregion private void ClearMenu() { if (mFiles.Count > 0) { //Se necessario, elimina il separatore. if ((mPosition + mFiles.Count) < mMenu.DropDownItems.Count) mMenu.DropDownItems.RemoveAt(mPosition + mFiles.Count - 1); //Elimina l'elenco dei file recenti. for (int i = (mPosition + mFiles.Count - 1); i >= mPosition; i--) mMenu.DropDownItems.RemoveAt(i); } } private void DrawMenu() { if (mFiles.Count > 0) { for (int i = 0; i < mFiles.Count; i++) { if (i >= mMaxMenuItems) { mFiles.RemoveAt(i); } else { ToolStripMenuItem item = new ToolStripMenuItem("&" + (i + 1) + " " + this.ShortenPathName(mFiles[i]), null, item_Click); //Nel tag dell'item è salvato il numero di ordine del file //recente. Questo serve perchè, dal comando di menu, sia //possibile risalire immediatamente al nome del file vero //e proprio. item.Tag = i; mMenu.DropDownItems.Insert(mPosition + i, item); } } //Se i file recenti non sono le ultime voci del menu, inserisce un //separatore dopo l'elenco. if ((mPosition + mFiles.Count) < mMenu.DropDownItems.Count) mMenu.DropDownItems.Insert(mPosition + mFiles.Count, new ToolStripSeparator()); } } private void item_Click(object sender, EventArgs e) { string file = mFiles[int.Parse(((ToolStripMenuItem)sender).Tag.ToString())]; //Lancia l'evento che segnala che è stato selezionato un file recente. if (mOnClick != null) mOnClick(this, file); } public void AddFile(string FileName) { if (mShowRecentFiles) { //Cancella l'elenco dei file recenti. this.ClearMenu(); } //Controlla se il file da aggiungere è già presente nella lista: in //questo caso, lo sposta in cima. int index = this.IndexOf(FileName); if (index != -1) mFiles.RemoveAt(index); //Aggiunge (o sposta) il file all'inizio della lista. mFiles.Insert(0, FileName); SHAddToRecentDocs(SHARD_PATH, FileName); //Se necessario, elimina i file "meno recenti". if (mFiles.Count > mMaxMenuItems) { for (int i = mMaxMenuItems; i < mFiles.Count; i++) mFiles.RemoveAt(i); } if (mShowRecentFiles) { //Aggiorna l'elenco dei file recenti. this.DrawMenu(); } } public int MaxMenuItems { get { return mMaxMenuItems; } set { if (value != mMaxMenuItems) { mMaxMenuItems = value; this.ClearMenu(); this.DrawMenu(); } } } public bool ShowRecentFiles { get { return mShowRecentFiles; } set { if (value != mShowRecentFiles) { mShowRecentFiles = value; if (mShowRecentFiles) { this.DrawMenu(); } else { this.ClearMenu(); } } } } private int IndexOf(string value) { //Utilizza questo metodo per evitare int i = 0; while (i < mFiles.Count) { if (string.Compare(mFiles[i], value, true) == 0) return i; i++; } return -1; } }}
Ad esempio, per visualizzare i 4 file recenti nel menu
File (mnuFile ), sopra l'ultimo comando (che tipicamente è
Esci), ed eseguire la
routine RecentFile_Click quando viene selezionato un documento,
è sufficiente scrivere:
RecentFiles Recents = new RecentFiles(mnuFile, mnuFile.DropDownItems.Count - 1, 4, RecentFile_Click); private void RecentFile_Click(RecentFiles sender, string FileName){ //Inserire il codice per aprire il file FileName.}La classe RecentFiles si occupa anche
di inserire il file selezionato nel menu Dati Recenti di
Windows. Quello che manca è il salvataggio dell'elenco dei file alla chiusura
dell'applicazione, ma può...
L'oggetto Directory contenuto nel namespace System.IO espone un metoto, Delete, che con un opportuno parametro consente di cancellare ricorsivamente tutte le directory, i file e le sottodirectory in essa contenute. Tuttavia, nel caso in cui un file abbia l'attributo di sola lettura, l'operazione fallisce. In questo caso è necessario ricorrere ad una cara funzione ricorsiva che, prima della cancellazione di ogni file, ne imposta l'attributo su Archivio:
using System;using System.IO;namespace SystemControl{ public static class Cleaner { private static void DeleteFiles(DirectoryInfo di, bool DeleteDirectory) { foreach (FileInfo f in di.GetFiles()) { try { //Imposta l'attributo di archivio per il file prima di eliminarlo. File.SetAttributes(f.FullName, FileAttributes.Archive); f.Delete(); } catch { } } foreach (DirectoryInfo d in di.GetDirectories()) { //Richiama ricorsivamente la funzione per eliminare tutte le sottodirectory. DeleteFiles(d, true); } if (DeleteDirectory) di.Delete(); } public static void Delete(DirectoryInfo di) { //Elimina tutti i file contenuti nella directory specificata. DeleteFiles(di, false); } public static void Delete(Directory path) { //Elimina tutti i file contenuti nella directory specificata. DeleteFiles(new DirectoryInfo(path), false); } }}
powered by IMHO 1.3
Per sapere quale Assembly sono usati dalla nostra
applicazione, è sufficiente invocare un metodo:
AssemblyName[] names = Assembly.GetEntryAssembly().GetReferencedAssemblies();foreach (AssemblyName name in names) lvwDipendenze.Items.Add(new ListViewItem(new string[] { name.Name, name.Version.ToString(3) }));
Questo esempio popola la ListView
lvwDipendenze, inserendo nella prima colonna il nome dell'assembly e
nella seconda la sua versione.
powered by IMHO 1.3
Il framwork .NET offre un metodo abbastanza semplice per creare un'applicazione senza interfaccia utente (ad esempio, un'applicazione che deve rispondere alla pressione di una HotKey, come ho mostrato in un post precedente). E' sufficiente creare una classe che estende ApplicationContext e richiamarla utilizzando il metodo Application.Run. Ad esempio, creiamo una nuova Windows Application, quindi rimuoviamo il file Form1.cs e inseriamo il codice sotto riportato nel file Program.cs, sostituendo quello esistente:
static class Program { static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Control()); } public class Control : ApplicationContext { private System.Threading.Timer timer; public Control() { timer = new System.Threading.Timer(new System.Threading.TimerCallback(TimerCallback), null, 5000, 5000); } private void TimerCallback(object state) { timer.Dispose(); Application.Exit(); } } }
Questo esempio si limita ad avviare un'applicazione senza interfaccia utente che attende cinque secondi, quindi termina la sua esecuzione. Lo schema qui illustrato ha comunque validità generale, e può essere riadattato...
Tavolta si ha l'esigenza di impedire l'esecuzione di più istanze della stessa applicazione. Visual Basic .NET, nella sua ultima versione, consente di aggiungere questa funzionalità semplicemente spuntando un'opzione del progetto. Con C#, invece, è necessario scrivere qualche riga di codice:
using System;using System.Diagnostics;using System.Runtime.InteropServices;using System.Threading;using System.Reflection;namespace SpecialServices{ //SingleProgamInstance uses a mutex synchronization object // to ensure that only one copy of process is running at // a particular time. It also allows for UI identification // of the intial process by bring that window to the foreground. public class SingleProgramInstance : IDisposable { #region Platform Invoke //Win32 API calls necesary to raise an unowned processs main window [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool ShowWindowAsync(IntPtr hWnd,int nCmdShow); [DllImport("user32.dll")] private static extern bool IsIconic(IntPtr hWnd); private const int SW_RESTORE = 9; #endregion private Mutex processSync; public SingleProgramInstance() : this(string.Empty) { } public SingleProgramInstance(string identifier) { processSync = new Mutex(false, Assembly.GetExecutingAssembly().GetName().Name + identifier); } ~SingleProgramInstance() { //Release mutex (if necessary) //This should have been accomplished using Dispose() this.Release(); } public bool IsSingleInstance { get { if (processSync.WaitOne(0, false)) return true; else return false; } } public void RaiseOtherProcess() { Process proc = Process.GetCurrentProcess(); // Using Process.ProcessName does not function properly when // the name exceeds 15 characters. Using the assembly name // takes care of this problem and is more accruate than other // work arounds. string assemblyName = Assembly.GetExecutingAssembly().GetName().Name; foreach (Process otherProc in Process.GetProcessesByName(assemblyName)) { //ignore this process if (proc.Id != otherProc.Id) { // Found a "same named process". // Assume it is the one we want brought to the foreground. // Use the Win32 API to bring it to the foreground. IntPtr hWnd = otherProc.MainWindowHandle; if (IsIconic(hWnd)) ShowWindowAsync(hWnd,SW_RESTORE); SetForegroundWindow(hWnd); return; } } } #region Implementation of IDisposable private void Release() { try { if (processSync.WaitOne(0, false)) { //If we own the mutex then release it so that // other "same" processes can now start. processSync.ReleaseMutex(); } processSync.Close(); } catch { } } public void Dispose() { //release mutex (if necessary) and notify // the garbage collector to ignore the destructor this.Release(); GC.SuppressFinalize(this); } #endregion }}
Questo codice fa uso di un oggetto Mutex per evitare che verificare se lo stesso processo è già in esecuzione: in questo caso, fornisce un metodo che consente di riportare la sua finestra in primo piano. Ecco un semplice esempio del suo utilizzo (da inserire nel metodo Main del file Program.cs):
using (SpecialServices.SingleProgramInstance spi = new SpecialServices.SingleProgramInstance()){ if (spi.IsSingleInstance) { //E' la prima esecuzione del programma. Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } else { //E' già attiva un'altra istanza del programma. La porta in primo piano. spi.RaiseOtherProcess(); }}
Questo esempio di codice è stato realizzato prendendo spunto dal suggerimento...
Capita spesso di dover provare al volo un piccolo
stralcio di codice, e in questi casi può essere noioso (oltre che lento) creare
una soluzione di Visual Studio, magari solo per testare un paio di righe. In
casi come questo, ci viene in aiuto un piccolo strumento freeware, Snippet Compiler. Una volta avviato, presenta una
finestra in cui è subito possibile iniziare a scrivere il codice da provare.
Fatto questo, con la pressione del tasto F5 si manda in esecuzione: tutti i
salvataggi sono fatti automaticamente dal programma. Sono supportati anche il
debug, la colorazione del codice e l'IntelliSense. Per...
Il Framework .NET non fornisce metodi che consentono di
registrare HotKey a livello di sistema. In questo caso ci viene
in aiuto il Platform Invoke:
using System;using System.Runtime.InteropServices;using System.Collections.Generic;using System.Windows.Forms;namespace HotKey{ public class HotKeyHandler : NativeWindow, IDisposable { #region Platform Invoke [Flags] public enum Modifiers { NONE = 0, MOD_ALT = 0x01, MOD_CONTROL = 0x02, MOD_SHIFT = 0x04, MOD_WIN = 0x08 } private static readonly int WM_HOTKEY = 0x0312; [DllImport("user32.dll")] private static extern bool RegisterHotKey(IntPtr hWnd, int Id, int fsModifiers, int vlc); [DllImport("user32.dll")] private static extern bool UnregisterHotKey(IntPtr hWnd, int Id); #endregion private struct HotKeyInfo { public Keys Key; public Modifiers Modifier; public HotKeyInfo(Keys Key, Modifiers Modifier) { this.Key = Key; this.Modifier = Modifier; } public static bool operator==(HotKeyInfo HotKey1, HotKeyInfo HotKey2) { return ((HotKey1.Key == HotKey2.Key) && (HotKey1.Modifier == HotKey2.Modifier)); } public static bool operator !=(HotKeyInfo HotKey1, HotKeyInfo HotKey2) { return ((HotKey1.Key != HotKey2.Key) || (HotKey1.Modifier != HotKey2.Modifier)); } public override bool Equals(object obj) { if (obj == null || GetType() != obj.GetType()) return false; return this == (HotKeyInfo)obj; } public override int GetHashCode() { return base.GetHashCode(); } } private static int mLastID = 0; private Dictionary<int, HotKeyInfo> mHotKeys = null; public HotKeyHandler() { this.CreateHandle(new CreateParams()); mHotKeys = new Dictionary<int, HotKeyInfo>(); } public bool Register(Keys Key) { return this.Register(Key, Modifiers.NONE); } public bool Register(Keys Key, Modifiers Modifier) { mLastID++; //Registra la HotKey. if (RegisterHotKey(this.Handle, mLastID, (int)Modifier, (int)Key) == true) { //Se la registrazione ha avuto esito positivo, aggiunge la hotkey all'hashtable. //Questo serve per sapere quale hotkey è stata premuta, nel caso in cui ne //siano state registrate più di una. mHotKeys.Add(mLastID, new HotKeyInfo(Key, Modifier)); return true; } return false; } public void Unregister(Keys Key, Modifiers Modifier) { HotKeyInfo hotKey = new HotKeyInfo(Key, Modifier); if (mHotKeys.ContainsValue(hotKey)) { foreach (int ID in mHotKeys.Keys) { if (mHotKeys[ID] == hotKey) { UnregisterHotKey(this.Handle, ID); mHotKeys.Remove(ID); return; } } } } public void Unregister() { foreach (int ID in mHotKeys.Keys) UnregisterHotKey(this.Handle, ID); mHotKeys.Clear(); mLastID = 0; } #region Dispose private void FreeResources() { try { this.Unregister(); if (this.Handle != IntPtr.Zero) this.DestroyHandle(); } catch { } } ~HotKeyHandler() { this.FreeResources(); } public void Dispose() { this.FreeResources(); GC.SuppressFinalize(this); } #endregion public delegate void HotKeyPressedEventHandler(object sender, HotKeyPressedEventArgs e); //Evento che viene generato quando si preme una HotKey. public event HotKeyPressedEventHandler HotKeyPressed; protected override void WndProc(ref System.Windows.Forms.Message m) { if (m.Msg == HotKeyHandler.WM_HOTKEY) { //hID contiene l'ID della HotKey premuta. int hID = m.WParam.ToInt32(); if (mHotKeys.ContainsKey(hID)) { if (this.HotKeyPressed != null) this.HotKeyPressed(this, new HotKeyPressedEventArgs(mHotKeys[hID].Key, mHotKeys[hID].Modifier)); else base.WndProc(ref m); } else { base.WndProc(ref m); } } else { base.WndProc(ref m); } } } public class HotKeyPressedEventArgs : EventArgs { private Keys mKey; private HotKeyHandler.Modifiers mModifier; public HotKeyPressedEventArgs(Keys Key, HotKeyHandler.Modifiers Modifier) { mKey = Key; mModifier = Modifier; } public Keys Key { get { return mKey; } } public HotKeyHandler.Modifiers Modifier { get { return mModifier; } } }}
Dopo aver aggiunto questa classe al proprio progetto, per
utilizzarla è sufficiente scrivere, ad esempio:
using HotKey;//...HotKeyHandler HotKey = new HotKeyHandler();HotKey.Register(Keys.F12);HotKey.Register(Keys.Q, HotKeyHandler.Modifiers.MOD_ALT);HotKey.HotKeyPressed += new HotKeyHandler.HotKeyPressedEventHandler(HotKey_HotKeyPressed);private void HotKey_HotKeyPressed(object sender, HotKeyPressedEventArgs e){ if (e.Key == Keys.F12) { //... } else if (e.Key == Keys.Q && e.Modifier == HotKeyHandler.Modifiers.MOD_ALT) { //... }}
Infine, per una corretta esecuzione, è bene richiamare il
metodo Dispose dell'oggetto HotKeyHandler dopo aver terminato di lavorare con
esso (tipicamente in fase di chiusura dell'applicazione).
powered by IMHO 1.3
|