Press "Enter" to skip to content

14 – MiniSqlAgent – User interface Tuning – Fare aprire una sola volta il Jobs Manager

In questo post andremo ad implementare il necessario a fare in modo che la finestra di base della MiniSqlAgentConsole non apra più di una volta l’interfaccia per la gestione dei Jobs. Per fare questo andremo ad implementare una classe entity Generic, una collection Generic, ed un metodo anonimo.

Introduzione

Se provate ad eseguire la MiniSqlAgentConsole relativa al post Implementare il Job Manager per la Console di MiniSqlAgent- Le modifiche a MiniSqlAgentConsole e cliccate 2 volte sull’opzione di menu relativa al Jobs Manager.

mini_sql_agent_07_menu_jobs

Vedrete apparire 2 tabs con l’interfaccia perfettamente funzionanti. Continuando a cliccare, aprirete ulteriori copie della finestra. Questo comportamento non è molto professionale per un interfaccia e soprattutto potrebbe provocare i problemi che avevamo cercato di evitare impedendo ad un Job di essere aperto più volte contemporaneamente.

mini_sql_agent_07_win_jobs

Pertanto, così come abbiamo impedito che un Job venga aperto 2 volte all’interno del Manager, dobbiamo impedire che il manager sia aperto 2 volte nella MainWindow. Per fare questo ci baseremo su quanto fatto nel post Una Console per la gestione di un servizio in WPF – Gestione Parametri applicativi per impedire l’apertura doppia delle finestre collegate alla console applicativa ed estenderemo la classe ChildWindow facendola divenire una classe Generic per poterla utilizzare per gestire collezioni di tipologie diverse di controlli.

In sintesi cosa faremo:

  • Sostituiremo ChildWindow con ChildObject<T>.
  • Sostituiremo ChildWindowsCollection con ChildWindowsCollection<T>.
  • Modificheremo la gestione delle Child Window in MiniSqlAgentConsole.
  • Aggiungeremo la gestione dei Child Tab in MiniSqlAgentConsole.

Eliminare e sostituire 2 classi nelle librerie di base, provocherà un Breaking Change nelle librerie, quindi creeremo un nuovo post per ospitarle sul sito per lo scaricamento delle librerie stesse.

mini_sql_agent_07_solution_01

Modifichiamo DnwBaseWpf e sostituiamo le due classi.

La classe ChildObject generica

public class ChildObject<T> where T : class
{
    private string mName;
    private T mWindow;
    public ChildObject(string name, T win)
    {
        mName = name;
        mWindow = win;
    }

    public string Name
    {
        get
        {
            return mName;
        }
        set
        {
            mName = value;
        }
    }

    public T Child
    {
        get
        {
            return mWindow;
        }
        set
        {
            mWindow = value;
        }
    }

}

Per creare una classe generic, ovvero una classe in cui sia possibile inserire una property tipizzabile in base alle esigenze è necessario dichiarare la classe come tale ed indicare qual’è il tipo di oggetto generic accettato dalla stessa, nel nostro caso è possibile usare qualsiasi tipo di classe .NET.

La collection ChildObjectsCollection generica

public class ChildObjectsCollection<T> : List<ChildObject<T>> where T:class
{
    public ChildObject<T> this[string objectName]
    {
        get
        {
            return (this.FirstOrDefault(item => item.Name == objectName));
        }
    }
}

E’ indispensabile che anche la collection sia un Generic per poter utilizzare degli elementi generic al suo interno, se così non fosse, non sarebbe possibile farlo.

La modifica a CloseableTabItem

public const string FLD_ParentTabControl = "ParentTabControl";

public TabControl ParentTabControl
{
    get
    {
        return (TabControl)this.GetValue(ParentTabControlProperty);
    }
    set
    {
        this.SetValue(ParentTabControlProperty, value);
    }
}

public static readonly DependencyProperty ParentTabControlProperty = 
    DependencyProperty.Register(
    FLD_ParentTabControl, typeof(TabControl), 
    typeof(CloseableTabItem), new FrameworkPropertyMetadata(null, 
    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

Abbiamo aggiunto al CloseableTabItem una property ove indicare chi è il TabControl che lo contiene, per permetterci di chiuderlo nell’event handler generico senza dover andare a cercare chi è il suo controllo owner.

public static readonly RoutedEvent CloseTabEvent =
    EventManager.RegisterRoutedEvent("CloseTab", RoutingStrategy.Direct,
        typeof(RoutedEventHandler), typeof(CloseableTabItem));

Abbiamo inoltre modificato l’evento CloseTab per modificare la routing strategy da Bubble a Direct, perché il close di un Tab deve essere riferito solo al suo diretto contenitore, altrimenti, il bubble lo propaga lungo il visual tree e potrebbe arrivare ad un altro closeable tab a livello superiore creando comportamenti errati.

Le modifiche a MainWindowModel

private ChildObjectsCollection<Window> mOpenWindows;

private ChildObjectsCollection<TabItem> mOpenTabs;

Le dichiarazioni, abbiamo sostituito ChildWindowsCollection con ChildObjectsCollection<Window> per la gestione delle finestre instanziabili una sola volta ed abbiamo aggiunto una seconda collezione di oggetti TabItem (Perché il CloseableTabItem è un discendente di TabItem).

public MainWindowModel()
{
    mOpenWindows = new ChildObjectsCollection<Window>();
    mOpenTabs = new ChildObjectsCollection<TabItem>();
......
}

Abbiamo istanziato le due collection nel costruttore del model.

public void AddChildTab(string childName, TabItem win)
{
    mOpenTabs.Add(new ChildObject<TabItem>(childName, win));
}

public void AddChildWindow(string childName, Window win)
{
    mOpenWindows.Add(new ChildObject<Window>(childName, win));
}

I due metodi per aggiungere alle collezioni un nuovo elemento.

public ChildObject<TabItem> GetChildTab(string childName)
{
    return (mOpenTabs[childName]);
}

public ChildObject<Window> GetChildWindow(string childName)
{
    return (mOpenWindows[childName]);
}

I due metodi per verificare l’esistenza ed acquisire un elemento se già inserito nella collection.

public void RemoveChildTab(string childName)
{
    ChildObject<TabItem> child = mOpenTabs[childName];
    if (child != null)
    {
        mOpenTabs.Remove(child);
    }
}

public void RemoveChildWindow(string childName)
{
    ChildObject<Window> child = mOpenWindows[childName];
    if (child != null)
    {
        mOpenWindows.Remove(child);
    }
}

I due metodi per rimuovere gli elementi dalle collection.

public void OpenJobsManager(TabControl owner)
{
    ChildObject<TabItem> child = GetChildTab(TAB_JobsManager);
    CloseableTabItem tab = null;
    if (child == null)
    {
        tab = new CloseableTabItem();
        ScrollViewer panel = new ScrollViewer();
        panel.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
        tab.Content = panel;
        tab.TabUniqueID = TAB_JobsManager;
        tab.Header = Properties.Resources.txtMWJobsManagerTab;
        tab.ParentTabControl = owner;
        MiniSqlAgentJobsManager jobM = new MiniSqlAgentJobsManager();

        panel.Content = jobM;

        tab.CloseTab += tbcMdi_CloseTab;

        owner.Items.Add(tab);
        AddChildTab(TAB_JobsManager, tab);
    }
    else
    {
        tab = (CloseableTabItem)child.Child;
    }
    tab.IsSelected = true;
}

Il metodo per aprire il Jobs Manager controllando che ne sia aperta una sola istanza, utilizza i metodi definiti in precedenza per controllare l’esistenza e creare il manager oppure attivarlo per riportarlo in primo piano.

private void tbcMdi_CloseTab(object sender, RoutedEventArgs e)
{
    CloseableTabItem item = sender as CloseableTabItem;
    item.ParentTabControl.Items.Remove(item);
    RemoveChildTab(item.TabUniqueID);
    e.Handled = true;
}

Utilizzando la dependency property ParentTabControl, siamo in grado di chiudere il Jobs Manager (o qualsiasi altro Tab Item) senza dover avere alcun riferimento che porta dal ViewModel alla Window e viceversa.

Modifichiamo anche il controllo MiniSqlAgentJobManagerControl

Considerato che abbiamo apportato alcune modifiche importanti ed introdotto le classi per il controllo dell’apertura singola, riportiamo le modifiche anche sul Job Manager per la gestione dell’apertura singola dei Jobs.

private ChildObjectsCollection<TabItem> mOpenTabs;
public MiniSqlAgentJobsManager()
{
    InitializeComponent();
    this.DataContext = this;
    mOpenTabs = new ChildObjectsCollection<TabItem>();
}

Introduciamo la collection dei tabs aperti che verrà generata nel costruttore della classe.

private void GenerateJobControl(TabControl owner, string jobFileName, bool editMode)
{
    ChildObject<TabItem> child = mOpenTabs[jobFileName];
    CloseableTabItem tab = null;
    if (child == null)
    {
        tab = new CloseableTabItem();
        Grid panel = new Grid();
        panel.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) });

        tab.Content = panel;
        tab.Header = Path.GetFileName(jobFileName);
        tab.ToolTip = jobFileName;
        tab.TabUniqueID = jobFileName;
        tab.ParentTabControl = owner;
        MiniSqlAgentJobControl jobC = new MiniSqlAgentJobControl();
        jobC.Closed += Job_Closed;
        jobC.Saved += Job_Saved;
        jobC.JobControlParent = tab;
        tab.Tag = jobC;
        panel.Children.Add(jobC);
        Grid.SetColumn(jobC, 0);
        Grid.SetRow(jobC, 0);
        tab.CloseTab += delegate  {
            bool exit = true;
            if( jobC.Modified )
            {
                if( MessageBox.Show( Properties.Resources.warMSAJCUnsavedEditsExitAnyway,  Properties.Resources.warGENWarning,  MessageBoxButton.YesNo,MessageBoxImage.Question )  != MessageBoxResult.Yes)
                {
                    exit = false;
                }

            }
            if( exit )
            {
                mOpenTabs.Remove(mOpenTabs[tab.TabUniqueID]);
                tab.ParentTabControl.Items.Remove(tab);

            }
        };
        tbcContent.Items.Add(tab);
        mOpenTabs.Add( new ChildObject<TabItem>(jobFileName, tab));
        jobC.Init(jobFileName);
        tab.IsSelected = true;
        OnPropertyChanged(FLD_IsTabVisible);

        //This is a trick to be sure that the control is set in edit mode after all
        //the operations on the visual tree of the context are finished
        this.Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(() =>
        {

            jobC.SetEditMode(editMode);
        }));

    }
    else
    {
        tab = (CloseableTabItem)child.Child;
        tab.IsSelected = true;
    }
}

Modifichiamo la generazione dei tab con le seguenti modifiche:

  • Modifichiamo la signature del metodo aggiungendo anche il parametro owner, per indicare il tab control contenitore.
  • Sostituiamo ExistTab, con la verifica dell’esistenza dell’oggetto Tab nella collection.
  • Se il tab esiste nella collezione degli open tabs, lo portiamo in primo piano.
  • Altrimenti: Facciamo alcune modifiche per migliorare l’aspetto dello user control nel tab:
    • Usiamo GridUnitType.Star per fare in modo che lo user control occupi tutto lo spazio all’interno del TabItem.
    • Mettiamo sul titolo (Header) solo il nome del file, mentre il path completo del Job lo inseriamo nel Tooltip per rendere la larghezza delle label dei TabItem più piccola.
  • Assegnamo l’owner al ParentTabControl.
  • Aggiungiamo un elemento alla collezione degli open tabs
  • Implementiamo una gestione corretta del tasto Close dei TabItem utilizzando un metodo anonimo che ci permette di fare in modo che anche la chiusura usando il tasto [X] controlli se vi sono state modifiche al Job prima di chiudere.
  • Vediamo le operazioni effettuate dal metodo anonimo:
    • Predispone un flag di controllo
    • Controlla la property Modified dello User Control del Job
    • Se ci sono state modifiche richiede la conferma dell’uscita e della perdita delle modifiche all’utente.
    • In base a quanto verificato, chiude il tab e gestisce la rimozione dalla collection dei tab aperti.
void Job_Closed(object sender, RoutedEventArgs e)
{
    MiniSqlAgentJobControl jobC = sender as MiniSqlAgentJobControl;
    if (jobC != null)
    {
        CloseableTabItem tabItem = jobC.JobControlParent as CloseableTabItem;
        mOpenTabs.Remove(mOpenTabs[tabItem.TabUniqueID]);
        tabItem.ParentTabControl.Items.Remove(tabItem);
    }
    e.Handled = true;
}

Modifichiamo l’event handler dell’evento closed dello User control del singolo job, in modo che utilizzi la collection degli open tabs e chiuda il tab corrente.

Le modifiche allo user control MiniSqlAgentJobControl

Abbiamo fatto alcuni aggiustamenti nello user control relativo ai Jobs di tipo estetico e funzionale, quindi listiamo le modifiche al codice XAML e al codice CSharp per spiegare le motivazioni:

<Grid Style="{StaticResource PanelEditMode}">
<Grid.RowDefinitions>
    <RowDefinition Height="50*"/>
    <RowDefinition Height="100*"/>

Abbiamo modificato l’altezza delle righe relative ai controlli Descrizione e SqlScript del controllo in modo che si allarghino in base alla dimensione della finestra in modo proporzionale (Descrizione = 1/2 sql script).

<TextBox Name="txtDescription" Grid.Row="0" 
    Grid.Column="1"      
    HorizontalAlignment="Stretch"      
    VerticalAlignment="Stretch"      
    Text="{Binding Job.Description}" 
    AcceptsReturn="True"      
    VerticalScrollBarVisibility="Auto"       
    MinHeight="32"       
    Margin="10,5,10,5"
    IsEnabled="{Binding IsInEditMode}"/>

Abbiamo modificato la textbox della descrizione in modo che il VerticalAlignment sia Stretch, ed abbiamo eliminato l’attributo Height lasciando solo la MinHeight.

<TextBox 
    Grid.Row="1" 
    Grid.Column="1"      
    HorizontalAlignment="Stretch"      
    VerticalAlignment="Stretch"      
    MinHeight="48"      
    AcceptsReturn="True"       
    Text="{Binding Job.SqlScript}"      
    TextWrapping="Wrap"      
    VerticalScrollBarVisibility="Auto"      
    Margin="10,5,10,5"
    IsEnabled="{Binding IsInEditMode}"/>

Anche la textbox dello script sql è stata modificata in modo che il VerticalAlignment sia Stretch ed abbiamo eliminato l’attributo Height e lasciato solo la minHeight.

public bool Modified
{
    get
    {
        return (mJobModel.Modified);
    }
}

Nel code behind abbiamo esposto la property Modified del Job Model in modo da poterla utilizzare nel metodo anonimo di controllo dell’evento Close del TabItem contenitore.

Conclusioni

Un post di Fine tuning che ci ha visto “lucidare” e rendere meno grezzo il funzionamento della nostra interfaccia utente. ed abbiamo visto alcune cose sempre utili al bagaglio di un programmatore, quali l’uso dei metodi anonimi e l’implementazione di classi e collezioni Generic.

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.