Press "Enter" to skip to content

6 – MiniSqlAgent – Aggiungere il supporto ai Messaggi HTTP tra un servizio di Windows e la sua Console

In questo post aggiungeremo la funzionalità di invio messaggi tramite HTTP al servizio MiniSqlAgent che abbiamo sviluppato nei post precedenti e la funzionalità di ricezione dei messaggi alla sua console di supporto. Per farlo aggiorneremo l’ HTTP sender, aggiorneremo il sistema di Log del servizio, aggiungeremo alcuni parametri di configurazione al servizio, aggiungeremo inoltre una Window che fornirà un real time monitor del servizio all’amministratore.

Modifichiamo la classe SenderHTTP

Come primo esperimento di trasmissione HTTP nel mondo reale, agganceremo il sistema di log del servizio alla console utilizzando HTTP, perciò modificheremo il gestore del log del servizio, ma già pensando a questo tipo di funzionamento ci possiamo rendere conto di un problema nella classe Sender, infatti, il nostro helper gestisce un protocollo di comunicazione e se il destinatario dei messaggi non esiste, genera una Exception e Logga il valore dell’Exception utilizzando il sistema standard di log:

public static string HTTPPost(HttpConnectionInfo connectionData, NameValueCollection parametrizerDataToSend, bool waitResponse = true)
{
	string result = string.Empty;
	HttpWebRequest txRequest = null;

	try
	{
....
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
	finally
	{
		if (txRequest != null)
		{
			try
			{
				txRequest.Abort();
			}
			catch { }

			txRequest = null;
		}
	}

	return (result);
}

Questo è il nostro metodo Post, se modificassimo l’event handler del Logger aggiungendogli la trasmissione HTTP dell’errore, nel caso il listener non fosse in ascolto si genererebbe un Loop di messaggi di errore con conseguente blocco del servizio stesso, esplosione del file di log ecc. ecc.

Pertanto andiamo a fare una modifica al codice del metodo post e del metodo get:

public static string HTTPPost(HttpConnectionInfo connectionData, 
	NameValueCollection parametrizerDataToSend, 
	bool waitResponse = true, bool ignoreWebErrors = false)
{
	string result = string.Empty;
	HttpWebRequest txRequest = null;

	try
	{
		if (parametrizerDataToSend != null)
		{
			string boundary = "----------------------------" + DateTime.Now.Ticks.ToString("x");

			ServicePointManager.Expect100Continue = false;

			txRequest = (HttpWebRequest)WebRequest.Create(connectionData.ConnectionString);
			txRequest.ContentType = "multipart/form-data; boundary=" + boundary;
			txRequest.Method = "POST";
			txRequest.KeepAlive = waitResponse;
			txRequest.Credentials = System.Net.CredentialCache.DefaultCredentials;
			txRequest.Timeout = connectionData.Timeout;
			txRequest.Date = DateTime.Now.ToUniversalTime();

			using (Stream memStream = new MemoryStream())
			{
				byte[] boundarybytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n");

				string formdataTemplate = "\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}";

				foreach (string key in parametrizerDataToSend.Keys)
				{
					string formitem = string.Format(formdataTemplate, key, parametrizerDataToSend[key]);
					byte[] formitembytes = System.Text.Encoding.UTF8.GetBytes(formitem);
					memStream.Write(formitembytes, 0, formitembytes.Length);
				}
				memStream.Write(boundarybytes, 0, boundarybytes.Length);

				string footer = "\r\n--" + boundary + "--\r\n";
				byte[] footerbytes = System.Text.Encoding.UTF8.GetBytes(footer);
				memStream.Write(footerbytes, 0, footerbytes.Length);

				txRequest.ContentLength = memStream.Length;

				using (Stream requestStream = txRequest.GetRequestStream())
				{
					memStream.Position = 0;
					byte[] tempBuffer = new byte[memStream.Length];
					memStream.Read(tempBuffer, 0, tempBuffer.Length);
					requestStream.Write(tempBuffer, 0, tempBuffer.Length);
					requestStream.Flush();
					requestStream.Close();
				}
				memStream.Close();
			}

			if (waitResponse)
			{
				result = GetResponse(txRequest);
			}
		}
	}
	catch (WebException wEx)
	{
		if (ignoreWebErrors)
		{
			result = string.Format("WEBERROR:{0}", wEx.Message);
		}
		else
		{
			EventLogger.SendMsg(wEx);
			throw;
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
	finally
	{
		if (txRequest != null)
		{
			try
			{
				txRequest.Abort();
			}
			catch { }

			txRequest = null;
		}
	}

	return (result);
}

In giallo abbiamo evidenziato le modifiche, abbiamo aggiunto un parametro per poter ignorare gli errori di connettività Web se necessario. In questo modo, se implementiamo un sistema di Log, che semplicemente trasmette dei dati al Listener se questo è in ascolto e se non ascolta non ha alcun problema imposteremo il flag a true, se invece dobbiamo parlare ad un servizio ed abbiamo bisogno di sapere se esiste, lasceremo il flag a false e le eccezioni le gestiremo a livello di programma.

public static string HTTPGet(HttpConnectionInfo connectionData, 
	NameValueCollection parametrizerDataToSend, 
	bool ignoreWebErrors = false)
{
	string result = string.Empty;
	HttpWebRequest txRequest = null;
	try
	{
		StringBuilder sbParams = new StringBuilder(connectionData.ConnectionString);
		if ((parametrizerDataToSend != null) && (parametrizerDataToSend.Count > 0))
		{
			string paramSeparator = "?";
			for (int i = 0; i < parametrizerDataToSend.Count; i++)
			{
				sbParams.Append(paramSeparator);
				sbParams.Append(parametrizerDataToSend.GetKey(i));
				sbParams.Append("=");
				sbParams.Append(HttpUtility.UrlEncode(parametrizerDataToSend.Get(i)));
				paramSeparator = "&";
			}
		}

		txRequest = (HttpWebRequest)WebRequest.Create(sbParams.ToString());
		txRequest.Method = "GET";
		txRequest.Credentials = System.Net.CredentialCache.DefaultCredentials;
		txRequest.Timeout = connectionData.Timeout;
		txRequest.Date = DateTime.Now.ToUniversalTime();

		result = GetResponse(txRequest);
	}
	catch (WebException wEx)
	{
		if (ignoreWebErrors)
		{
			result = string.Format("WEBERROR:{0}", wEx.Message);
		}
		else
		{
			EventLogger.SendMsg(wEx);
			throw;
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
	finally
	{
		if (txRequest != null)
		{
			try
			{
				txRequest.Abort();
			}
			catch { }

			txRequest = null;
		}
	}

	return (result);
}

Anche il metodo Get è stato modificato allo stesso modo.

Le modifiche alla classe MiniSqlAgentService

private string GetLogMessage(SendMessageEventArgs e)
{
	StringBuilder sb = new StringBuilder();
	if (e.MessageType == MessageType.Error)
	{
		sb.AppendLine("*** ERROR! ***");
	}
	if (e.MessageType == MessageType.Warning)
	{
		sb.AppendLine("+++ WARNING! +++");
	}
	sb.AppendFormat(string.Format("{0} {1}: {2}", DateTime.Now.ToShortDateString(), DateTime.Now.ToLongTimeString(), e.MessageToLog));
	sb.AppendLine();
	return (sb.ToString());
}

Modifichiamo il metodo che compone i messaggi da loggare o inviare via HTTP.

void EventLogger_LogNewEntry(object sender, SendMessageEventArgs e)
{
	if (e.MessageType == MessageType.Error)
	{
		Trace.Write(GetLogMessage(e));
		Trace.Flush();
	}
	else
	{
		if (ServiceContext.Instance.SettingsManager.LogVerbosity == LogType.AllMessages)
		{
			Trace.Write(GetLogMessage(e));
			Trace.Flush();
		}
		else
		{
			if (ServiceContext.Instance.SettingsManager.LogVerbosity == LogType.WarningsAndExceptions)
			{
				if (e.MessageType == MessageType.Warning)
				{
					Trace.Write(GetLogMessage(e));
					Trace.Flush();
				}
			}
		}
	}
	if (ServiceContext.Instance.SettingsManager.ActivateHTTPLog)
	{
		NameValueCollection nvc = new NameValueCollection();
		nvc.Add("Type", e.MessageType.ToString());
		nvc.Add("Message", GetLogMessage(e));
		SenderHTTP.HTTPPost(ServiceContext.Instance.SettingsManager.HTTPConnection,
			nvc, false, true);
	}
}

E modifichiamo l’event handler del Logger aggiungendo l’invio via HTTP.  Siamo sicuri abbiate notato che l’invio via HTTP è subordinato ad un flag booleano che si trova nei parametri di configurazione del servizio, oltre a questo flag, abbiamo anche una classe HttpConnectionInfo che fornisce le informazioni di connessione permettendo di parametrizzarle,

Modifiche alla classe ServiceSettingsManager

Vediamo come abbiamo modificato quindi la classe ServiceSettingsManager per aggiungere il supporto alla nuova funzionalità.

protected override void LoadAppSettings()
{
...

		mAppSettings.Add(new DnwSetting()
		{
			Category = Properties.Resources.txtSSMCatConfiguration,
			ID = STT_ActivateHTTPLog,
			Description = Properties.Resources.txtSSMActivateHTTPLog,
			EditorType = EditorType.CheckBox,
			Position = 1,
			Value = true.ToString(),

		});

		mAppSettings.Add(new DnwSetting()
		{
			Category = Properties.Resources.txtSSMCatConfiguration,
			ID = STT_HTTPAddress,
			Description = Properties.Resources.txtSSMHTTPAddress,
			EditorType = EditorType.TextBox,
			Position = 2,
			Value = "127.0.0.1",

		});

		mAppSettings.Add(new DnwSetting()
		{
			Category = Properties.Resources.txtSSMCatConfiguration,
			ID = STT_HTTPPort,
			Description = Properties.Resources.txtSSMHTTPPort,
			EditorType = EditorType.NumericInteger,
			Position = 3,
			Value = "10001",

		});

...
}

I puntini corrispondono al codice che abbiamo già discusso nel post Aggiungere alcuni parametri di configurazione ad un Servizio Windows in questo caso, per modificare il nostro progetto ci siamo limitati ad aggiungere 3 nuovi DnwSetting, il boolean per l’attivazione della trasmissione HTTP, l’indirizzo HTTP del listener e la porta su cui il listener ascolta. I default impostati sono:

  • Attiva la trasmissione http
  • Trasmetti su localhost (assume che il listener sia sulla macchina locale)
  • Trasmetti sulla porta 10001 – è consigliato dalle linee guida Microsoft di utilizzare porte sopra alla 10000 per le applicazioni aziendali.
public bool ActivateHTTPLog
{
	get
	{
		bool activateHttpLog = true;
		bool.TryParse(mAppSettings[STT_ActivateHTTPLog].Value, out activateHttpLog);
		return activateHttpLog;
	}
}

public HttpConnectionInfo HTTPConnection
{
	get
	{
		if (mHttpConnection == null)
		{
			mHttpConnection = new HttpConnectionInfo();
			mHttpConnection.Address = mAppSettings[STT_HTTPAddress].Value;
			int httpPort = 0;
			int.TryParse(mAppSettings[STT_HTTPPort].Value, out httpPort);
			mHttpConnection.Port = httpPort;
		}
		return mHttpConnection;
	}
}

Le property che espongono i valori dei parametri di configurazione al servizio, possiamo notare che abbiamo creato un’ HttpConnectionInfo a partire dai due parametri indirizzo e porta.

service_console_04_01[4]

Come viene automaticamente generata l’interfaccia utente per i parametri di configurazione dall’introduzione dei nuovi parametri, abbiamo utilizzato anche il campo Position dei DnwSetting per decidere arbitrariamente come ordinare la lista dei parametri sulla Window.

Le modifiche alla console del servizio

Per aggiungere le funzionalità di monitoraggio del servizio alla nostra Console, dobbiamo innanzitutto modificare la MainWindow per aggiungere un opzione di menu relativa alla nuova Window, e poi dobbiamo creare una Window che sia in grado di ascoltare e visualizzare i messaggi inviati dal servizio via HTTP. Vediamo che cosa abbiamo modificato per ottenere tutto questo.

Le modifiche alla classe ToolsCommands

Per aprire la nuova finestra Monitor, abbiamo bisogno di un RoutedCommand, considerato che potremo costruire molte applicazioni con servizi o comunque che forniscano funzionalità di trasmissione HTTP abbiamo deciso di creare il command nella classe ToolsCommands delle nostre librerie di base, ecco come l’abbiamo modificata:

public static class ToolsCommands
{
	public static readonly RoutedCommand OpenConfig = new RoutedCommand();

	public static readonly RoutedCommand OpenMonitor = new RoutedCommand();
}

Il nuovo command permette di aprire una finestra di tipo Monitor, dove veder loggare l’attività di una applicazione o un servizio.

Le modifiche alla classe MainWindow

Nella classe MainWindow abbiamo aggiunto la nuova opzione di Menu ed il nuovo command nello XAML:

	<Window.CommandBindings>
...
		<CommandBinding  	Command="{x:Static gcmds:ToolsCommands.OpenMonitor}"
			Executed="OpenMonitor_Executed"/>
...
	</Window.CommandBindings>

La modifica ai command Bindings.

		<Menu Grid.Row="0" Grid.ColumnSpan="2"  Margin="0"    VerticalAlignment="Top" BorderThickness="2"   Foreground="Black" FontSize="16" FontWeight="Normal">
...
			<MenuItem  Header="{x:Static p:Resources.mnuMWTools}"  	VerticalAlignment="Center" Margin="10,2,10,2"  	Padding="10,4,10,4"  Height="32" >
...
				<MenuItem  Header="{x:Static p:Resources.mnuMWActivityLog}"  	VerticalAlignment="Center"  	Command="{x:Static gcmds:ToolsCommands.OpenMonitor}"  	Margin="2,4,2,2" Height="32" >
					<MenuItem.Icon>
						<Image Source="/MiniSqlAgentConsole;component/Images/btn_032_724.png"  	Height="24" Width="24"/>
					</MenuItem.Icon>
				</MenuItem>
			</MenuItem>
		</Menu>

La nuova opzione di menu.

private void OpenMonitor_Executed(object sender, ExecutedRoutedEventArgs e)
{
	mWindowModel.OpenActivityLog(this);
}

L’event handler del command.

public void OpenActivityLog(Window owner)
{
	ChildWindow child = GetChild(WIN_ActivityLog);
	ActivityLogWindow win = null;
	if (child == null)
	{
		win = new ActivityLogWindow();
		win.Icon = owner.Icon;
		win.Owner = owner;
		win.Closed += delegate { RemoveChild(WIN_ActivityLog); };
		AddChild(WIN_ActivityLog, win);
		win.Show();
	}
	else
	{
		win = (ActivityLogWindow)child.Window;
		win.BringIntoView();
	}
}

La gestione della nuova Window con il necessario affinché venga aperta una sola volta, rimando chi non ha letto i post precedenti al post Una Console per la gestione di un servizio in WPF – Parte 2 La Console il manager e la User Interface ed eventualmente al suo post collegato per la spiegazione estesa di come abbiamo realizzato il sistema per evitare che una finestra possa essere aperta due volte in una applicazione anche se non è modale.

La Finestra ActivityLogWindow

<Window x:Class="Dnw.MiniSqlAgent.Console.Windows.ActivityLogWindow"         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:gcmds="clr-namespace:Dnw.Base.Wpf.Commands;assembly=Dnw.Base.Wpf.v4.0"         Title="Service Activity Log" Height="480"  Width="640" Loaded="Window_Loaded" Closed="Window_Closed">
	<Window.Resources>
		<ResourceDictionary>
			<ResourceDictionary.MergedDictionaries>
				<ResourceDictionary  Source="pack://application:,,,/Dnw.Base.Wpf.v4.0;component/Styles.xaml"/>
			</ResourceDictionary.MergedDictionaries>
		</ResourceDictionary>
	</Window.Resources>
	<Window.CommandBindings>
		<CommandBinding  	Command="{x:Static gcmds:SemaphoreCommands.Start}"
			CanExecute="Start_CanExecute"
			Executed="Start_Executed"/>
		<CommandBinding  	Command="{x:Static gcmds:SemaphoreCommands.Stop}"
			CanExecute="Stop_CanExecute"
			Executed="Stop_Executed"/>
		<CommandBinding  	Command="ApplicationCommands.Delete"
			CanExecute="Clear_CanExecute"
			Executed="Clear_Executed"/>
	</Window.CommandBindings>
	<Grid Margin="0, 0, 0, 0" >
		<Grid.RowDefinitions>
			<RowDefinition Height="48"/>
			<RowDefinition Height="*" />
			<RowDefinition Height="48"/>
		</Grid.RowDefinitions>
		<Grid.ColumnDefinitions>
			<ColumnDefinition Width="*"/>
		</Grid.ColumnDefinitions>
		<Grid Grid.Row="0">
			<Grid.RowDefinitions>
				<RowDefinition Height="Auto"/>
			</Grid.RowDefinitions>
			<Grid.ColumnDefinitions>
				<ColumnDefinition Width="*"/>
				<ColumnDefinition Width="Auto"/>
				<ColumnDefinition Width="Auto"/>
			</Grid.ColumnDefinitions>
			<TextBlock Grid.Row="0" Grid.Column="0"  Text="The Activity log monitor for the service"  Style="{StaticResource MainInstruction}"   HorizontalAlignment="Left"  VerticalAlignment="Center" Margin="5,2,5,2"/>
			<TextBlock Grid.Row="0" Grid.Column="1"  Text="{Binding Status}"  Style="{StaticResource Header}"   HorizontalAlignment="Left"  VerticalAlignment="Center" Margin="5,2,5,2"/>
			<Image  Grid.Row="0" Grid.Column="2"  Width="32" Height="32" Margin="5,2,5,2"  Source="{Binding StatusImage}"  VerticalAlignment="Center"/>
		</Grid>
		<Grid Grid.Row="1">
			<Grid.RowDefinitions>
				<RowDefinition Height="Auto" />
				<RowDefinition Height="*" />
			</Grid.RowDefinitions>
			<Grid.ColumnDefinitions>
				<ColumnDefinition Width="*"/>
			</Grid.ColumnDefinitions>
			<TextBlock Text="Results" FontFamily="Segoe UI"  FontSize="12" FontWeight="Bold"  Foreground="WhiteSmoke" Background="Navy"  HorizontalAlignment="Stretch"  VerticalAlignment="Stretch" />
			<TextBox Grid.Row="1" Grid.Column="0"  Text="{Binding Result}"/>
		</Grid>
		<StackPanel Height="48" Grid.Row="2" Grid.Column="0"  FlowDirection="RightToLeft" Orientation="Horizontal">
			<Button  Margin="10,5,10,5" Padding="25,5,25,5"  VerticalAlignment="Center"  Command="{x:Static gcmds:SemaphoreCommands.Start}" >Start</Button>
			<Button  Margin="10,5,10,5" Padding="25,5,25,5"  Command="{x:Static gcmds:SemaphoreCommands.Stop}" 
				VerticalAlignment="Center" >Stop</Button>
			<Button  Margin="10,5,10,5" Padding="25,5,25,5"  Command="ApplicationCommands.Delete"   VerticalAlignment="Center" >Clear Result</Button>
		</StackPanel>
	</Grid>
</Window>

Lo XAML della Window non contiene nulla che non sia stato preventivamente discusso nei post dedicati a WPF in questo Blog, pertanto vi rimando alla lista principale dei Post dedicati a WPF.

public partial class ActivityLogWindow : Window, INotifyPropertyChanged
{
 
	ListenerHTTP mListener;
 	
	private string mResult;
 
	public ActivityLogWindow()
	{
		InitializeComponent();
		this.DataContext = this;
	}

La dichiarazione della classe e le sue variabili, abbiamo aggiunto l’interfaccia INotifyPropertyChanged perché la classe farà da View Model a se stessa, abbiamo creato una variabile per il listener ed una per inserire i risultati di quanto viene trasmesso dal servizio. Ed abbiamo indicato nel costruttore che la window è view model di se stessa.

private void Window_Closed(object sender, EventArgs e)
{
	if (mListener != null)
	{
		if (mListener.IsListening)
		{

			mListener.Stop();
			mListener = null;
		}
	}
}

private void Window_Loaded(object sender, RoutedEventArgs e)
{
	mListener = new ListenerHTTP(ServiceContext.Instance.SettingsManager.HTTPConnection,  System.Threading.ThreadPriority.Normal, autoStart: false);
	mListener.RequestReceived += RequestReceived;
	UpdateStatus();
	if (!ServiceContext.Instance.SettingsManager.ActivateHTTPLog)
	{
		UpdateResult(Properties.Resources.txtALWTheHTTPLoggerIsNotActive);
	}
	else
	{
		UpdateResult(Properties.Resources.txtALWStartMessage);
	}
}

I due eventi di Caricamento e chiusura della Window, al load della Window Istanziamo il Listener e aggiorniamo lo stato della window indicando all’utente se il servizio ha l’opzione di invio dati in HTTP attiva.
Alla chiusura della finestra, verifichiamo se il listener è attivo e lo fermiamo in modo che non rimangano oggetti appesi dopo la sua chiusura.

public BitmapImage StatusImage
{
	get
	{
		BitmapImage result = new BitmapImage(new Uri("pack://application:,,,/MiniSqlAgentConsole;component/Images/btn_032_134.png"));
		if (mListener != null)
		{
			if (mListener.IsListening)
			{
				result = new BitmapImage(new Uri("pack://application:,,,/MiniSqlAgentConsole;component/Images/btn_032_135.png")); ;
			}
		}
		return (result);
	}
}

public string Status
{
	get
	{
		return mStatus;
	}
	set
	{
		mStatus = value;
		OnPropertyChanged(FLD_Status);
	}
}

public String Result
{
	get
	{
		return mResult;
	}
	set
	{
		mResult = value;
		OnPropertyChanged(FLD_Result);
	}
}

Le tre property in binding sullo XAML che aggiornano il testo e l’immagine che mostra lo stato del listener ed aggiornano la finestra con il risultato delle trasmissioni HTTP.

private void Clear_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
	e.CanExecute = mResult != null && mResult.Length == 0;
}

private void Clear_Executed(object sender, ExecutedRoutedEventArgs e)
{
	mResult = null;
}

Gli Event handler del Command Clear, collegato al bottone corrispondente sulla finestra, questo Button è attivo solo se la variabile Result non è vuota.

private void Start_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
	e.CanExecute = ServiceContext.Instance.SettingsManager.ActivateHTTPLog
		&& mListener != null && !mListener.IsListening;
}

private void Start_Executed(object sender, ExecutedRoutedEventArgs e)
{
	mListener.Start(System.Threading.ThreadPriority.Normal);
	UpdateStatus();
}

Gli event handler del command Start, che permettono di far partire il listener solo quando è fermo.

private void Stop_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
	e.CanExecute = mListener != null && mListener.IsListening;
}

private void Stop_Executed(object sender, ExecutedRoutedEventArgs e)
{
	mListener.Stop();
	UpdateStatus();
}

Gli event handler del command Stop, che permettono di fermare il listener solo se sta ascoltando.

private string RequestReceived(HTTPDataReceivedEventArgs e)
{
	StringBuilder sb = new StringBuilder();
	foreach (string key in e.FieldsCollection.AllKeys)
	{
		sb.Append(e.FieldsCollection[key]);
	}
	UpdateResult(sb.ToString());
	return ("FATTO"); //The response for the GET and POST to send to the sender
}

Il delegate del metodo che il listener esegue ogni volta che riceve dei dati, abbiamo composto e restituito una riga per il response, ma nel nostro caso, non è indispensabile in quanto abbiamo usato un semplice POST che non ha bisogno di risposta.

private void UpdateResult(string resultData)
{
	Result = resultData + Environment.NewLine + Result;
}

private void UpdateStatus()
{
	if (mListener != null)
	{
		Status = mListener.IsListening ?
			Properties.Resources.txtALWStatusListening :
			Properties.Resources.txtALWStatusNotListening;
		UpdateResult(string.Format(
			Properties.Resources.txtALWStatusFormat,
			Status));
	}
	else
	{
		Status = Properties.Resources.txtALWStatusNotInit;
	}
	OnPropertyChanged(FLD_Result);
	OnPropertyChanged(FLD_StatusImage);
}

I due metodi che aggiornano il contenuto della textbox result e lo status del listener aggiornando anche la stringa e l’immagine di stato. Ed è tutto per il nostro progetto.

Abbiamo creato un piccolo webcast che mostra come funziona quanto fino a qui abbiamo creato.

Potete trovare il progetto di esempio usato per questo archivio al link seguente:

Il progetto delle librerie comuni utilizzate in questo progetto è dispobibile al link seguente:

Per qualsiasi domanda, curiosità , approfondimento, potete usare il link alla form di contatto in cima alla pagina.