Press "Enter" to skip to content

16 – MIniSqlAgent – Modificare i Job per poter gestire il Timeout

In quanto sviluppato fino ad ora per creare un servizio in grado di eseguire script SQL su un Sql Server in modo schedulato, abbiamo predisposto tutto quanto per una esecuzione molto semplice, lasciando al client ADO e alla configurazione del Server SQL il compito di decidere come eseguire gli script. In questo post, aggiungeremo un parametro di controllo fondamentale per i Jobs, ovvero la possibilità di decidere dopo quanto tempo la connessione andrà in timeout se non riuscirà a completare un Job.

Si tratta di un parametro fondamentale che serve ad evitare il tipo di errore qui sotto riportato.

Error
*** ERROR! ***
17/06/2013 16:11:31: --------------------------------------------------------------------------------
Exception Time: 17/06/2013 16:11:31
--------------------------------------------------------------------------------
Exception type:System.Data.SqlClient.SqlException
--------------------------------------------------------------------------------
Message: Timeout expired.  The timeout period elapsed prior to completion of the operation or the server is not responding.
The backup or restore was aborted.
10 percent processed.
20 percent processed.
30 percent processed.
40 percent processed.
--------------------------------------------------------------------------------
StackTrace:
   at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at System.Data.SqlClient.SqlCommand.RunExecuteNonQueryTds(String methodName, Boolean async, Int32 timeout)
   at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, String methodName, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite)
   at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   at Dnw.Base.Data.SqlServer.SqlHelper.ExecuteNonQuery(String connectionString, String sqlCommand, SqlParameter[] sqlParameters, Int32 commandTimeout) in h:\Yeye\TheRecipesProject\Code\DnwLibraries\DnwBaseDataSqlServer\SqlHelper.cs:line 65
   at Dnw.MiniSqlAgent.ServiceLayer.JobManager.JobExecute(MiniSqlAgentJob job) in h:\Yeye\TheRecipesProject\Code\MiniSqlAgent\MiniSqlAgentServiceLayer\JobManager.cs:line 362

--------------------------------------------------------------------------------

Questo è quanto mostrato dalla console di  controllo del MiniSqlAgent impostando come Job il backup full di un database piuttosto grande, fra quelli di test che io ho sul mio server.

Come si può notare il backup arriva al 40% e poi abortisce, questo perché il command timeout impostato per default sul server da ADO.Net non è capiente a sufficienza, infatti un backup di questo tipo può aver bisogno di qualche minuto per essere eseguito e usualmente il timeout standard è di un minuto.

Per modificare il sistema ed essere in grado di gestire Job di lunga durata, vediamo che cosa intendiamo fare:

  • Modifichiamo i Setting del servizio per permettere all’utente di gestire il massimo numero di thread concorrenti avviati dal servizio stesso per eseguire i Job.
  • Modifichiamo l’oggetto MiniSqlAgentJob per aggiungere un campo per il timeout.
  • Modifichiamo l’oggetto JobManager del servizio per aggiungere la gestione del timeout.
  • Applichiamo un lifting degli User Control e delle window, e trasferiamo tutte le stringhe dei messaggi e delle descrizioni dei controlli sul file Resources.resx.
    • I messaggi relativi al controllo del singolo job hanno la forma pfxMSAJCDescription
    • I messaggi relativi al controllo Job Manager hanno la forma pfxMSAJMDescription
    • I messaggi relativi alla main window hanno la forma pfxMWDescription
      pfx è un prefisso che riguarda il tipo di messaggio (txt=testo, war=avviso, exc=exception, mnu=opzione menu per citarne alcuni) La sigla in maiuscolo prende le lettere maiuscole del nome della classe sorgente. Non è una nomenclatura obbligatoria, è arbitraria ed è stata scelta da noi, ciascuno può costruire la propria nel modo più consono e cofortevole.
  • Aggiungiamo le icone ai bottoni sullo user control del Job per rendere visualmente più chiare le loro funzioni.
  • Aggiungiamo alle librerie di base un controllo per dare un messaggio di attesa quando eseguiamo del codice SQL.
  • Aggiungiamo allo user control per la gestione dei Job la possibilità di testare la correttezza sintattica di uno script e quella di eseguirlo realmente su SQLServer.

La classe ServiceSettingsManager

Questa modifica non è strettamente attinente all’aggiunta del timeout di esecuzione dei Job, però è importante, perché da modo all’amministratore di decidere qual’è il carico di lavoro che intende dare al server da parte del nostro servizio, infatti se usassimo il MiniSqlAgent per gestire molti Job potrebbe essere utile poterne avviare più di uno per volta. Vediamo come si può fare.

protected override void LoadAppSettings()
{
    try
    {

Nel metodo LoadAppSettings introduciamo il nuovo parametro di configurazione

mAppSettings.Add(new DnwSetting()
{
    Category = Properties.Resources.txtSSMCatConfiguration,
    ID = STT_MaxExecutionJobThreads,
    Description = Properties.Resources.txtSSMJobsMaxExecutionJobThreads,
    EditorType = EditorType.SqlConnectionsFile,
    Position = 30,
    Value = "1",

});

Il nuovo parametro di configurazione per default impostato a 1 permette di decidere se il servizio può lanciare uno o più Job contemporaneamente durante il suo funzionamento. Questo ci permetterà di fare in modo che quando le operazioni effettuate fossero non collegate l’una all’altra, lo schedulatore possa far partire ad esempio 2 diversi Backup di database alla stessa ora. Si tratta di un parametro che può dare una mano agli amministratori che controllano molte risorse contemporaneamente.

public int MaxExecutionJobThreads
{
    get
    {
        int threads = 1;
        int.TryParse(mAppSettings[STT_MaxExecutionJobThreads].Value, out threads);
        return threads;
    }
}

La property che espone il numero dei threads ammissibili.

Le modifiche alla classe JobManager

public void StartThreading()
{
    try
    {

....
        mJobExecutionSemaphore = new SemaphoreSlim(ServiceContext.Instance.SettingsManager.MaxExecutionJobThreads);
....
    }
    catch (Exception ex)
    {
        EventLogger.SendMsg(ex);
    }
}

La modifica al metodo di startup del servizio per indicare al Semaforo di gestione dell’esecuzione dei Job il numero di thread concorrenti massimo eseguibile.

private void JobExecute(MiniSqlAgentJob job)
{
    try
    {
....

    SqlHelper.ExecuteNonQuery(cnInfo.ConnectionString,  job.SqlScript, commandTimeout:job.CommandTimeout);
....
    }
....
}

La modifica all’esecuzione dei Job in cui passiamo all’esecutore su SQL Server il Timeout del Command.

La modifica alla classe MiniSqlAgentJob

[XmlElement(ElementName = "CommandTimeout")]
public int CommandTimeout
{
    get
    {
        return mCommandTimeout;
    }
    set
    {
        mCommandTimeout = value;
        OnPropertyChanged(FLD_CommandTimeout);
    }
}

[XmlIgnore]
public bool IsExecuting
{
    get
    {
        return mIsExecuting;
    }
    set
    {
        mIsExecuting = value;
        OnPropertyChanged(FLD_IsExecuting);
    }
}

Due property che abbiamo aggiunto ai nostri Job, la prima è il timeout del command, che ci permetterà di modificare il valore per i job di lunga durata in modo che il sistema non vada in errore. La seconda è un flag che viene settato quando il Job viene preso in carico per l’esecuzione e resettato alla fine dell’esecuzione, in modo che se un Job ha una lunga durata non accada che venga accodato più volte per l’esecuzione.

public MiniSqlAgentJob()
{
    this.CommandTimeout = -1;
}

Modifichiamo il costruttore inizializzando il timeout al valore di default.

public void CopyTo(MiniSqlAgentJob job)
{
    job.ConnectionID = this.ConnectionID;
    job.Description = this.Description;
    job.ExecutionInterval = this.ExecutionInterval;
    job.JobFileName = this.JobFileName;
    job.NextExecutionTime = this.NextExecutionTime;
    job.SqlScript = this.SqlScript;
    job.IsExecuting = this.IsExecuting;
    job.CommandTimeout = this.CommandTimeout;

}

Modifichiamo il CopyTo per aggiungere i due nuovi oggetti.

Le modifiche a MiniSqlAgentJobModel

private const string SQL_TEST_FMP = @"
SET NOEXEC ON
{0}
SET NOEXEC OFF
";

public void TestSqlScript()
{
    try
    {

        string commandToTest = string.Format(SQL_TEST_FMP, this.Job.SqlScript);
        SqlConnectionInfo cnInfo = ServiceContext.Instance.SqlConnections[this.Job.ConnectionID];
        if (cnInfo != null)
        {
            SqlHelper.ExecuteNonQuery(cnInfo.ConnectionString, commandToTest);
            StatusManager.Notify(this, Properties.Resources.txtMSAJMSyntaxOk);
        }
        else
        {
            StatusManager.Notify(this, Properties.Resources.txtMSAJMConnectionInfoKO);
        }

    }
    catch (Exception ex)
    {
        EventLogger.SendMsg(ex);
        MessageBox.Show(ex.Message);
    }
}


public void ExecuteSqlScript()
{
    try
    {

        SqlConnectionInfo cnInfo = ServiceContext.Instance.SqlConnections[this.Job.ConnectionID];
        if (cnInfo != null)
        {
            SqlHelper.ExecuteNonQuery(cnInfo.ConnectionString, this.Job.SqlScript, commandTimeout:Job.CommandTimeout);
            StatusManager.Notify(this, Properties.Resources.txtMSAJMExecutionOk);
        }
        else
        {
            StatusManager.Notify(this, Properties.Resources.txtMSAJMConnectionInfoKO);
        }

    }
    catch (Exception ex)
    {
        EventLogger.SendMsg(ex);
        MessageBox.Show(ex.Message);
    }
}

Nel model dello user control relativo al singolo Job, aggiungiamo i due metodi per testare la sintassi ed eseguire lo script SQL inserito nel Job sul SQL Server indicato come Connessione per il Job. I due metodi sono identici fatto salvo per una piccola cosa, per testare uno script senza eseguirlo veramente, SQL Server fornisce la clausola SET NOEXEC (on oppure off), indicandola prima di eseguire uno statement, il sistema fingerà la sua esecuzione dando eventuale errore nel caso lo script fosse sintatticamente non corretto.

Le modifiche a MiniSqlAgentJobControl

xmlns:lcmds="clr-namespace:Dnw.MiniSqlAgent.Console.Commands"
xmlns:gctrl="clr-namespace:Dnw.Base.Wpf.Controls;assembly=Dnw.Base.Wpf.v4.0"
xmlns:p="clr-namespace:Dnw.MiniSqlAgent.Console.Properties"

Partiamo con le modifiche effettuate allo XAML dello user control: Abbiamo aggiunto 3 namespaces, il primo per accedere ai commands specifici di MiniSqlAgent, il secondo per accedere agli UserControl della libreria di base. Il terzo per agganciare le stringhe delle resources applicative per la gestione corretta dei testi della finestra.

Loaded="UserControl_Loaded">

Abbiamo aggiunto l’event handler per l’evento Loaded dello User Control e vedremo perché nel code behind.

<UserControl.CommandBindings>

.... qui c'erano i command già gestiti

    <CommandBinding      Command="{x:Static lcmds:MiniSqlAgentCommands.TestSqlScript}"
        CanExecute="TestSqlScript_CanExecute"
        Executed="TestSqlScript_Executed"/>
    <CommandBinding      Command="{x:Static lcmds:MiniSqlAgentCommands.ExecuteSqlScript}"
        CanExecute="ExecuteSqlScript_CanExecute"
        Executed="ExecuteSqlScript_Executed"/>
</UserControl.CommandBindings>

Abbiamo aggiunto i command per i due nuovi button che inseriremo nello User Control.

Text="{x:Static p:Resources.txtMSAJCDescription}" Margin="10" />

Riportiamo una delle stringhe agganciate ora alle resources come esempio, ma tutte le stringhe dello User Control sono ora delle Resources applicative.

<TextBlock Grid.Row="0" Grid.Column="0"      Text="{x:Static p:Resources.txtMSAJCSqlScript}" Margin="10" />
    <Grid Grid.Row="1" Grid.Column="0">
        <Grid.RowDefinitions>
            <RowDefinition Height="50*"/>
            <RowDefinition Height="50*"/>
            <RowDefinition Height="50*"/>
            <RowDefinition Height="50*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="30*"/>
            <ColumnDefinition Width="50*"/>
        </Grid.ColumnDefinitions>
        <Button 
            Grid.Row="2" 
            Grid.Column="1" 
            Margin="10,2,10,2" 
            Padding="0,0,0,0"      
            Command="{x:Static 
            lcmds:MiniSqlAgentCommands.TestSqlScript}"       
            VerticalAlignment="Center"      
            ToolTip="{x:Static p:Resources.ttpMSAJCTest}">
            <StackPanel 
                FlowDirection="LeftToRight" 
                Orientation="Horizontal">
                <TextBlock Text="{x:Static p:Resources.cmdMSAJCTest}"      
                    Margin="10,2,5,2"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center" Width="50"/>
                <Image      
                    Source="/MiniSqlAgentConsole;component/Images/btn_032_736.png"      
                    Height="24" Width="24"
                Margin="5,2,10,2"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"/>

            </StackPanel>
        </Button>
        <Button Name="btnExecute" 
            Grid.Row="3" 
            Grid.Column="1" 
            Margin="10,2,10,2" 
            Padding="0,0,0,0"      
            Command="{x:Static lcmds:MiniSqlAgentCommands.ExecuteSqlScript}"       
            VerticalAlignment="Center"      
            ToolTip="{x:Static p:Resources.ttpMSAJCExecute}">
            <StackPanel 
                FlowDirection="LeftToRight" 
                Orientation="Horizontal">
                <TextBlock Text="{x:Static p:Resources.cmdMSAJCExecute}" 
                    Margin="10,2,5,2"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center" Width="50"/>
                <Image      
                    Source="/MiniSqlAgentConsole;component/Images/btn_032_467.png"      
                    Height="24" Width="24"
                Margin="5,2,10,2"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"/>
            </StackPanel>
        </Button>
    </Grid>

</Grid>

Le modifiche alla riga relativa alla gestione dello script SQL per aggiungere i button per la verifica e l’esecuzione degli script. Nella casella dove prima c’era solo il testo relativo all’etichetta dello script SQL abbiamo creato una grid ed abbiamo inserito i due buttons Test ed Execute per il comando sql.

<TextBlock 
    Grid.Row="5" 
    Grid.Column="0"      
    Text="{x:Static p:Resources.txtMSAJCCommandTimeout}" 
    Margin="10" />
<wix:IntegerUpDown  
    FormatString="G0"
    Grid.Row="5" Grid.Column="1"      
    HorizontalAlignment="Left" 
    Width="80"      
    VerticalAlignment="Center"      
    Value="{Binding Job.CommandTimeout}"       
    Margin="10,5,10,5"
    IsEnabled="{Binding IsInEditMode}"/>

I controlli per la gestione del timeout, una label ed un’IntegerUpDown agganciato alla variabile del Job.

<StackPanel 
    Height="42" 
    Grid.Row="0" 
    Grid.Column="1"      
    FlowDirection="RightToLeft" 
    Orientation="Horizontal" >
    <Button  
        Margin="10,2,10,2"       
        VerticalAlignment="Center"      
        Command="ApplicationCommands.Save"      
        ToolTip="{x:Static p:Resources.ttpMSAJCSave}">
        <StackPanel 
            FlowDirection="LeftToRight" 
            Orientation="Horizontal">
            <TextBlock 
                Text="{x:Static p:Resources.cmdMSAJCSave}" 
                Margin="10,2,5,2"
                VerticalAlignment="Center"
                HorizontalAlignment="Center" 
                Width="50"/>
            <Image      
                Source="/MiniSqlAgentConsole;component/Images/btn_032_367.png"      
                Height="24" Width="24"
                Margin="5,2,10,2"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"/>
        </StackPanel>
    </Button>
    <Button  
        Margin="10,2,10,2"
        Command="ApplicationCommands.Close"       
        VerticalAlignment="Center"      
        ToolTip="{x:Static p:Resources.ttpMSAJCClose}">
        <StackPanel 
            FlowDirection="LeftToRight" 
            Orientation="Horizontal">
            <TextBlock 
                Text="{x:Static p:Resources.cmdMSAJCClose}" 
                Margin="10,2,5,2"
                VerticalAlignment="Center"
                HorizontalAlignment="Center" 
                Width="50"/>
            <Image 
                Source="/MiniSqlAgentConsole;component/Images/btn_032_734.png"      
                Height="24" Width="24"
                Margin="5,2,10,2"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"/>
        </StackPanel>                
    </Button>
    <Button  
        Margin="10,2,10,2"      
        Command="{x:Static gcmds:EditingCommands.Edit}"       
        VerticalAlignment="Center"      
        ToolTip="{x:Static p:Resources.ttpMSAJCEdit}">
        <StackPanel 
            FlowDirection="LeftToRight" 
            Orientation="Horizontal">
            <TextBlock 
                Text="{x:Static p:Resources.cmdMSAJCEdit}" 
                Margin="10,2,5,2"
                VerticalAlignment="Center"
                HorizontalAlignment="Center" 
                Width="50"/>
            <Image      
                Source="/MiniSqlAgentConsole;component/Images/btn_032_113.png"      
                Height="24" Width="24"
                Margin="5,2,10,2"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"/>
        </StackPanel>
    </Button>
</StackPanel>

Le modifiche ai command button per mettere le stringhe nelle risorse ed aggiungere le icone che rendano chiaro l’uso dei button.

<gctrl:WaitMePopupControl Name="waitMe" Visibility="Collapsed"/>

Ultimo ma non meno importante, inseriamo nello XAML lo user control per la popup window di attesa e proseguiamo con le modifiche al code behind.

private void TestSqlScript_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    if (this.mJobModel != null)
    {
        e.CanExecute = this.mJobModel.Job.IsValid;
    }
}

private void TestSqlScript_Executed(object sender, ExecutedRoutedEventArgs e)
{
    this.mJobModel.TestSqlScript();
}

La gestione del tasto di test della correttezza dello script SQL può essere eseguito se il Job risulta valido ed esegue il metodo che abbiamo creato all’interno del Model per questa funzionalità.

private void ExecuteSqlScript_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    if (this.mJobModel != null)
    {
        e.CanExecute = this.mJobModel.Job.IsValid;
    }
}

private void ExecuteSqlScript_Executed(object sender, ExecutedRoutedEventArgs e)
{
    waitMe.ShowMe();
    this.IsEnabled = false;
    BackgroundWorker bw = new BackgroundWorker();
    bw.DoWork += bw_DoWork;
    bw.RunWorkerCompleted += bw_RunWorkerCompleted;
    
    bw.RunWorkerAsync();
}

La gestione del tasto di esecuzione dello script, anche in questo caso, può essere eseguito solo se il Job risulta valido, quando lo eseguiamo,  apriamo la finestra di popup che mostra il messaggio di attesa e disattiviamo lo user control. Poi lanciamo l’esecuzione dello script utilizzando un background worker per evitare di bloccare tutta l’applicazione.

void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    waitMe.HideMe();
    this.IsEnabled = true;
}


void bw_DoWork(object sender, DoWorkEventArgs e)
{
    this.mJobModel.ExecuteSqlScript();
}

I due Event handler del background worker, l’esecuzione dello script e la chiusura della finestra di popup al termine dell’esecuzione.

private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
    waitMe.MessageText = Properties.Resources.txtMSAJCExecuting;
    waitMe.PopupHeight = 200;
    waitMe.PopupWidth = 200;
    waitMe.PlacementTarget = this;
    waitMe.CenterInParent(this.ActualWidth, this.ActualHeight);
}

Nell’evento Loaded dello User control abbiamo inserito la predisposizione della finestra di popup per il messaggio di attesa per l’esecuzione degli script alla creazione dello user control.

Le modifiche a MiniSqlAgentCommands

public static readonly RoutedCommand TestSqlScript = new RoutedCommand();

public static readonly RoutedCommand ExecuteSqlScript = new RoutedCommand();

I due nuovi commands che abbiamo aggiunto alla classe che contiene i commands pertinenti la console del servizio.

Il risultato finale sulla User Interface

mini_sql_agent_08_job_control_01[7]

Come appare la nuova finestra del Job.

mini_sql_agent_08_job_control_02[7]

Come appare la finestra quando il sistema sta eseguendo il Job.

Il Popup Control per l’attesa

Lo User control della schermata di attesa, è stato inserito sulle librerie di base, si tratta di un controllo piuttosto semplice, a cui abbiamo inserito una piccola animazione per fare in modo che l’utente sappia che il sistema sta lavorando e non è bloccato. La gestione delle animazioni sugli elementi della user interface, in WPF è un argomento molto interessante e molto ampio, che non approfondiremo in questo blog se non per le piccole cose che ci serviranno ora ed in futuro. In questo caso, l’animazione utilizzata per muovere le lettere della scritta “Wait…” è stata ricavata da uno degli esempi di codice relativi a WPF scaricabili sul sito di MSDN.

Lo XAML del controllo Wait

<Grid >
<Popup 
    Name="popWait"     
    IsOpen="False"     
    StaysOpen="False"     
    PopupAnimation="Slide"    
    HorizontalOffset="10"    
    VerticalOffset="10"    
    AllowsTransparency="True"    
    Width="300"     
    Height="200">
    <Grid >
        <Grid.RowDefinitions>
            <RowDefinition Height="50*"/>
            <RowDefinition Height="35*"/>
        </Grid.RowDefinitions>
        <Border 
            Margin="5,5,5,5" 
            Background="White"      
            Grid.RowSpan="2" 
            BorderBrush="LightGray"      
            BorderThickness="3" 
            CornerRadius="10">
            <Border.Effect>
                <DropShadowEffect 
                    Color="DarkGray" Opacity="50"/>
            </Border.Effect>
        </Border>
        <TextBlock 
            Name="txtMessage" 
            Grid.Row="0"      
            FontSize="14"      
            Margin="20"
            Foreground="Black"
            Text="{Binding MessageText}"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"/>
        <StackPanel 
            Grid.Row="1" 
            Margin="40,10,40,20">
            <Border 
                Name="TextBorder" 
                HorizontalAlignment="Left"
                VerticalAlignment="Top" 
                Background="White">
                <TextBlock      
                    Name="RealText"       
                    FontFamily="Segoe UI"
                    FontSize="18 px"         
                    Margin="10"
                    Foreground="Black" 
                    Text="Wait...">
                

Lo User control ci serve esclusivamente come contenitore per il controllo Popup, pertanto non vi inseriamo altro, nel controllo Popup inseriamo una Grid con 2 righe, una per il messaggio parametrico, la seconda per il messaggio di attesa. Il messaggio di attesa contiene un Border che serve a stabilire il suo background e il messaggio di testo, che in questo caso non rendiamo traducibile perché l’animazione dipende dal numero di caratteri.

<TextBlock.TextEffects>

    <!-- The TextEffect to animate. -->
    <TextEffect PositionCount="1" x:Name="MyTextEffect">
        <TextEffect.Transform>
            <RotateTransform x:Name="TextEffectRotateTransform"      Angle="0" CenterX="10" CenterY="10" />
        </TextEffect.Transform>
    </TextEffect>
</TextBlock.TextEffects>

La definizione dell’effetto che permette di far eseguire ad un carattere una rotazione.

<TextBlock.Triggers>
    <EventTrigger 
        RoutedEvent="TextBlock.Loaded">
        <BeginStoryboard>
            <Storyboard>
                <ParallelTimeline RepeatBehavior="Forever">

                    <!-- Animates the angle of the RotateTransform
    applied to the TextEffect. -->
                    <DoubleAnimation
                    Storyboard.TargetName="TextEffectRotateTransform"
                    Storyboard.TargetProperty="Angle"      
                    From="0"
                    To="360"
                    Duration="00:00:0.75"                     
                    BeginTime="0:0:0.25" />
                </ParallelTimeline>

La definizione del trigger che attiva l’animazione e lo storyboard che descrive come l’animazione viene costruita. e l’animazione della proprietà Angle del testo che sarà modificata da 0 a 360 in 0.75 secondi iniziando 0.25 secondi dopo il trigger.

<!-- Animates the horizontal center of the RotateTransform
    applied to the TextEffect. -->
<DoubleAnimation
    From="00"
    To="360"
    Duration="01:30:0"
    RepeatBehavior="Forever"
    AutoReverse="True"
    Storyboard.TargetName="TextEffectRotateTransform"
    Storyboard.TargetProperty="CenterX" />

La seconda animazione, che fa riferimento all’effetto definito all’inizio della serie dei dati di costruzione dell’animazione indicando che l’animazione in questione anche in questo caso l’angolo relativo al testo, ruota da 0 a 360 si ripete senza fine e fa un autoreverse, questa animazione serve per la rotazione del singolo carattere.

                    <!-- Animates the position of the TextEffect. -->
                    <Int32AnimationUsingKeyFrames
                    Storyboard.TargetName="MyTextEffect"
                    Storyboard.TargetProperty="PositionStart"
                    Duration="0:0:7"
                    AutoReverse="True"
                    RepeatBehavior="Forever">
                    <Int32AnimationUsingKeyFrames.KeyFrames>
                        <DiscreteInt32KeyFrame Value="0" KeyTime="0:0:0" />
                        <DiscreteInt32KeyFrame Value="1" KeyTime="0:0:1" />
                        <DiscreteInt32KeyFrame Value="2" KeyTime="0:0:2" />
                        <DiscreteInt32KeyFrame Value="3" KeyTime="0:0:3" />
                        <DiscreteInt32KeyFrame Value="4" KeyTime="0:0:4" />
                        <DiscreteInt32KeyFrame Value="5" KeyTime="0:0:5" />
                        <DiscreteInt32KeyFrame Value="6" KeyTime="0:0:6" />
                    </Int32AnimationUsingKeyFrames.KeyFrames>
                </Int32AnimationUsingKeyFrames>  </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</TextBlock.Triggers>

L’ultima animazione serve per spostare l’animazione di un carattere per volta sulla scritta, è un animazione discreta ed è il motivo per cui non tradurremo la scritta Wait, visto che il DiscreteInt32KeyFrame deve essere definito per ogni carattere. Vi sono molti esempi e soluzioni che spiegano in dettaglio come creare animazioni e soprattutto come costruire animazioni dinamiche sul testo, ma visto che non sono parte di quanto ci interessa in modo specifico per gli sviluppi di questo blog, vi invitiamo a scaricare gli esempi dal sito di MSDN e studiarli se siete interessati a questo tipo di attività. A noi bastava ottenere un popup di attesa non proprio banale.

Vediamo il code behind

public partial class WaitMePopupControl : UserControl, INotifyPropertyChanged
{
    public WaitMePopupControl()
    {
        InitializeComponent();
        this.DataContext = this;

    }

Abbiamo agganciato lo user control come model di se stesso.

public UIElement PlacementTarget
{
    get
    {
        return popWait.PlacementTarget;
    }
    set
    {
        popWait.PlacementTarget = value;
    }
}

Iniziamo ad esporre le property del Popup control che ci permetteranno di configurarlo in base alle necessità nei vari luoghi ove lo useremo.

Il Placement target è il Controllo della UI a cui il Popup Viene agganciato, ricordiamo che il popup è simile al tooltip, va agganciato ad un controllo visuale per essere mostrato, se guardate la configurazione  dello User control dei Job, lo abbiamo agganciato proprio allo USer Control.

public void CenterInParent(double actualWidth, double actualHeight)
{
    double centerwidth = actualWidth / 2;
    double centerHeight = -actualHeight / 2;
    popWait.HorizontalOffset = centerwidth - (popWait.Width / 2) - 10;
    popWait.VerticalOffset = centerHeight - (popWait.Height / 2) - 10;
}

Un metodo che calcola la posizione del popup per metterlo più o meno nel centro del controllo a cui è collegato.

public void ShowMe()
{
    popWait.IsOpen = true;
    popWait.StaysOpen = true;

}

public void HideMe()
{
    popWait.IsOpen = false;
    popWait.StaysOpen = false;

}

I metodi per mostrare e nascondere il Popup

Oltre a questo, nel code behind troverete una serie di property che espongono le property dei controlli per permettere la configurazione delle dimensioni e del contenuto del nostro popup.

Il codice del progetto Minisqlagent con le modifiche relative a questo articolo è disponibile al link seguente:

Il codice delle librerie di uso comune collegate a questo articolo è disponibile al link seguente:

Per qualsiasi domanda, approfondimento o curiosità , usate il link al modulo di contatto oppure il link al forum Microsoft dove troverete me ed altri esperti in grado di rispondervi.