MSDN Tips & Tricks

I consigli degli esperti italiani per sfruttare al meglio gli strumenti di sviluppo e semplificare l’attività quotidiana.

In questa pagina

Consigli sull’utilizzo di GUID come Primary Key Consigli sull’utilizzo di GUID come Primary Key
Contare i giorni lavorativi tra due date Contare i giorni lavorativi tra due date
Supportare il Drag&Drop nelle applicazioni WPF Supportare il Drag&Drop nelle applicazioni WPF
Team Foundation Server: Sbloccare un file in Lock esclusivo da parte di un altro utente Team Foundation Server: Sbloccare un file in Lock esclusivo da parte di un altro utente

Consigli sull’utilizzo di GUID come Primary Key

Di Davide Mauri - Microsoft MVP

E’ generalmente sconsigliato l’utilizzo di una colonna GUID come Primary Key costruita su un indice cluster (l’impostazione di default).

La scelta di usare un GUID come Primary Key viene normalmente giustificata sostenendo che è necessario avere un valore che sia univoco anche tra database diversi, magari isolati e non connessi tra di loro.

In realtà, per questo tipo di esigenza, e possibile evitare l’utilizzo di un guid ed invece utilizzare due colonne di tipo int. Prima di vedere la soluzione per questo caso, però, è bene capire perché l’utilizzo di un guid deve essere un scelta ponderata.

Una colonna di tipo uniqueidentifier occupa ben 16 byte e ciò va contro la best practice di avere invece un indice cluster piccolo, in quanto utilizzato internamente in ogni indice non-cluster presente sulla tabella; oltre a questo, inoltre, la struttura BTree di qualsiasi indice (cluster o non-cluster) costruito su una colonna uniqueidentifier risulta essere grossa il doppio di un indice costruito su due colonne int e grossa il quadruplo di uno costruito su una singola colonna int. Un BTree grosso significa avere un indice molto meno efficiente di quanto invece potrebbe essere.

Oltre al problema delle dimensioni, inoltre, c’è il problema della non sequenzialità dei valori generati dalla funzione NEWID(), tipicamente utilizzata per generare valori per le colonne uniqueidentifier. Questa caratteristica crea uno “stress” maggiore a carico dell’indice che, dovendo mantenere ordinati i valori al suo interno, deve fare un lavoro maggiore perché i valori che invece gli arrivano della funzione NEWID() sono casuali. Ciò provoca una maggior necessità di eseguire dei “page-split” per poter accogliere i dati nell’ordine corretto, andando ad impattare negativamente sulle prestazioni. Il tutto è ancor più marcato se l’indice è di tipo cluster, che ad ogni page-split deve muovere intere righe di tabella e non solo i dati dell’indice. In particolare questo si traduce in un’altra frammentazione dei dati che deve essere tenuta a bada da una più frequente reindicizzazione delle tabelle.

Delineati i problemi, le soluzioni sono due: evitare l’utilizzo delle colonne uniqueidentifier ove possibile, oppure utilizzare la nuova funzione NEWSEQUENTIALGUID().

La prima possibilità è quella da preferirsi: nel caso in cui si debbano generare dei valori univoci anche tra database che non si possono “parlare”, è sufficiente creare una Primary Key composta da due colonne di tipo intero: la prima colonna identifica il database, la seconda la riga stessa. Facciamo un esempio pratico: al posto che avere questa situazione

create table dbo.Ordini
(
id_ordine uniqueidentifier not null default(newid()) primary key,
data_ordine datetime not null default (getdate()),
descrizione varchar(50) not null
)

È preferibile avere questa:

create table dbo.Ordini
(
id_installazione int not null,
id_ordine int not null identity,
data_ordine datetime not null default (getdate()),
descrizione varchar(50) not null,
primary key (id_installazione, id_ordine)
)

Nel secondo caso sarà nostra cura far si che il valore di “id_installazione” sia diverso per ogni installazione effettuata, ed ecco quindi che ogni riga sarà globalmente univoca. (nel caso non di dovessero fare più di 32000-e-oltre installazioni, si può usare uno smallint che permette di risparmiare altri 2 bytes).

Questa strada non è, però, sempre percorribile, e in alcune situazioni (ad esempio nell’uso della replica merge) è necessario l’uso di un GUID.

In tal caso l’uso della nuova funzione NEWSEQUENTIALID() deve essere preferito all’uso di NEWID(). La prima, infatti, genera dei valori che sono sequenziali, aiutando quindi l’indice a sostenere gli INSERT, evitando cosi i page-split:

create table dbo.Ordini
(
id_ordine uniqueidentifier not null default(newsequentialid()) primary key,
data_ordine datetime not null default (getdate()),
descrizione varchar(50) not null
)
Go

Il risultato, dopo alcuni inserimenti di prova è:

*

E si riescono cosi a conciliare (almeno in parte) due necessità – utilizzo di uniqueidentifier da una parte e buone performance dall’altra – altrimenti problematiche da far convivere.

 

Contare i giorni lavorativi tra due date

Di Davide Mauri - Microsoft MVP

Alcune delle più grosse problematiche per chi lavora con SQL Server si racchiudono nei meandri della gestione delle date e delle ore. (Per tutti coloro che sono ancora in difficoltà con il tipo di dato “datetime” ricordo che nell’articolo SQL Server 2005 Development Guidelines – Parte prima si tratta il tema in modo dettagliato.)

Tra queste, un richiesta abbastanza frequente è quella di sapere quanti giorni feriali ci sono tra due date.

Per poter risolvere il problema in modo definitivo abbiamo bisogno di due tabelle: una che conterrà il calendario vero e proprio, ed una che invece conterrà solo le festività:

create table dbo.GiorniLavorativi 
(
data datetime primary key not null,
lavorativo tinyint not null
)
go

create table dbo.GiorniDiFesta
(
mese tinyint not null,
giorno tinyint not null,
festivita varchar(100) not null,
primary key (mese, giorno)
)
go

E’ ora necessario riempire la tabella GiorniLavorativi, con tutte le date del periodo che vogliamo analizzare. Diciamo che nel nostro esempio vogliamo analizzare le date dal 2005 in avanti (fino al 2010 compreso). Per riempire la suddetta utilizzando le capacità proprie di un database (avremmo potuto fare un ciclo di “insert” ma sarebbe poco performante) abbiamo la necessità di usare una tabella di numeri. Tale tabella si può ottenere usando il metodo proposto da Itzik Ben-Gan per la generazione a run-time della stessa:

create function dbo.fn_Nums(@m as bigint) returns table
as
return
with
t0 as (select n = 1 union all select n = 1),
t1 as (select n = 1 from t0 as a, t0 as b),
t2 as (select n = 1 from t1 as a, t1 as b),
t3 as (select n = 1 from t2 as a, t2 as b),
t4 as (select n = 1 from t3 as a, t3 as b),
t5 as (select n = 1 from t4 as a, t4 as b),
result as (select row_number() over (order by n) as n from t5)
select n from result where n < @m
go

Possiamo ora utilizzare la funzione dbo.fn_Nums per generare date dal 2005-01-01 al 2010-12-31. Come? Semplicemente ricordandoci che sommando “1” ad un valore datetime si aumenta la data di un giorno.

Una banalissima

select
cast('20050101' as datetime) + n - 1
from
dbo.fn_nums(6*366)
where
cast('20050101' as datetime) + n - 1 <= '20101231'
go

in meno di un quarto di secondo (sul mio Intel Centrino 2.13 Ghz) genera tutte le date necessarie.

Prima di inserire tali date nella tabella GiorniLavorativi dobbiamo anche capire come stabilire se una data cade di sabato o di domenica (supponendo di dover considerare questi giorni come festivi).

Grazie alla funzione DATEPART si risolve il problema: questa funzione ci permette sapere, tra le altre cose, qual è il numero del giorno della settimana di una certa data. Ad esempio:

set language italiano

print datepart(weekday, '20070518')

stampa il valore “5”, ossia la data corrisponde al quinto giorno dall’inizio della settimana, e quindi a Venerdi. Il comando iniziale serve per assicurarci di stabilire come giorno d’inizio della settimana quello comunemente usato in Italia, ossia il Lunedi.

Se avessimo fatto la stessa prova ma con le impostazioni inglesi avremmo ottenuto il valore “6”, in quanto la settimana in questo caso inizia la Domenica anziché il Lunedi.

Per non avere problemi con le varie differenti impostazioni internazionali è sufficiente applicare formula riportata di seguito:

print datepart(weekday, cast('20070518' as datetime) + @@datefirst - 1)

grazie alla funzione di sistema @@datefirst possiamo ricondurre tutti i valore restituiti dalla funzione DATEPART ad avere il Lunedi come giorno di partenza della settimana, senza dover utilizzare la funzione SET LANGUAGE che modifica anche altre impostazioni oltre che il giorno di inizio settimana e che quindi potrebbe non essere utilizzabile in tutte le situazioni.

Stabilito come determinare se una data cade di Sabato (giorno numero 6 dall’inizio della settima, avendo Lunedi come primo giorno della stessa) o di Domenica (giorno numero 7 della settimana), ci rimane ora da definire quali sono le festività nazionali, in modo da poterne tenere conto durante il calcolo dei giorni lavorativi tra due date. Per farlo andiamo a riempire la tabella GiorniDiFesta:

insert into dbo.GiorniDiFesta values (1,1,'Capodanno')
insert into dbo.GiorniDiFesta values (1,6,'Epifania')
insert into dbo.GiorniDiFesta values (4,25,'Festa della liberazione')
insert into dbo.GiorniDiFesta values (5,1,'Festa del lavoro')
insert into dbo.GiorniDiFesta values (6,2,'Festa della Repubblica')
insert into dbo.GiorniDiFesta values (8,15,'Ferragosto')
insert into dbo.GiorniDiFesta values (11,1,'Tuttisanti')
insert into dbo.GiorniDiFesta values (12,25,'Natale')
insert into dbo.GiorniDiFesta values (12,26,'S. Stefano')
go

Ed ora non resta che scrivere la query finale, che inserirà nella tabella GiorniLavorativi tutte le date dal 2005-01-01 al 2010-12-31, impostando il valore della colonna “lavorativo” a 1 per tutti i giorni feriali ed a “0” per tutti i giorni festivi:

with cte as
(
select 
d = cast('20050101' as datetime) + n - 1
from
dbo.fn_nums(6*366)
where
cast('20050101' as datetime) + n - 1 < '20101231'
)
,cte2 as
(
select
data = d,
mese = month(d),
giorno = day(d),
lavorativo = case datepart(weekday, d + @@DATEFIRST - 1)
when 6 then 0 
when 7 then 0 
else 1
end
from
cte
)
insert into
dbo.GiorniLavorativi
select
c.data,
lavorativo = case when f.festivita is not null then 0 else lavorativo end
from
cte2 c
left outer join
dbo.GiorniDiFesta f on c.mese = f.mese and c.giorno = f.giorno
go

ed ecco quindi che per avere la lista di tutti i giorni lavorativi dal 2007-05-01 al 2007-05-31 è sufficiente fare due semplici e performanti query:

select *, datename(weekday, data) 
from dbo.GiorniLavorativi 
where data >= '20070501' and data < '20070601'

Se invece è necessario avere solo la quantità:

select sum(lavorativo) 
from dbo.GiorniLavorativi 
where data >= '20070501' and data < '20070601'

Com’è possibile immaginare la soluzione vista può facilmente essere adattata per altri usi, come ad esempio la gestione delle ferie, il calcolo dei giorni effettivi di produzioni e/o sviluppo, e via dicendo.

 

Supportare il Drag&Drop nelle applicazioni WPF

Di Cristian Civera - Microsoft MVP

L'uso del mouse è oggi giorno ampiamente diffuso ed è ormai comune la pratica di sfruttare l'interfaccia a finestre per compiere operazioni di vario genere semplicemente catturando un elemento e spostandolo in un'altra applicazione. Questo gesto, conosciuto come Drag&Drop, è molto comodo e se supportato anche nel proprio applicativo, può renderlo più completo e di più facile utilizzo per l'utente.

Windows Presentation Foundation rende questa operazione da finestre esterne piuttosto semplice, interfacciando con le API Win32, dotando ogni UIElement o ContentElement di una proprietà AllowDrop. Se impostata a True abilita quattro eventi e i loro relativi "preview" che permettono di controllare l'operazione di Drag&Drop sull'oggetto in questione. Gli eventi sono:

  • DragEnter: il mouse entra per la prima volta sull'elemento durante un'operazione di Drag&Drop;

  • DragLeave: il mouse ha lasciato l'elemento;

  • DragOver: il mouse si sta spostando sopra l'elemento (viene richiamato più volte);

  • Drop: è stato rilasciato il pulsante per l'operazione di Drag&Drop sopra l'elemento.

Gli eventi minimi da intercettare sono DragEnter e Drop e lo si può fare direttamente in XAML:

<ListBox x:Name="list" AllowDrop="True" DragEnter="ListBox_DragEnter" Drop="ListBox_Drop">
</ListBox>

Nell'evento DragEnter bisogna inanzitutto informare la sorgente e il motore di quali operazioni intendiamo dare supporto, agendo sulla proprietà Effects del DragEventArgs passato:

C#

void ListBox_DragEnter(object sender, DragEventArgs e)
{
    // Verifico che sia un dato valido
    e.Effects = (e.Data.GetDataPresent(DataFormats.FileDrop) ?
DragDropEffects.Move | DragDropEffects.Link : DragDropEffects.None);
    e.Handled = true;
}

VB

Sub ListBox_DragEnter(sender As Object, e As DragEventArgs)
    ' Verifico che sia un dato valido
    e.Effects = IIF(e.Data.GetDataPresent(DataFormats.FileDrop),
DragDropEffects.Move | DragDropEffects.Link, DragDropEffects.None)
    e.Handled = True
End Sub

Quando un'operazione di Drag&Drop ha inizio, la sorgente imposta nell'oggetto e.Data di tipo IDataObject uno o più valori in differenti formati, alcuni dei quali sono standard e conosciuti, mentre altri possono essere personalizzati. Il tipo DataFormats.FileDrop arriva quando si esegue un'operazione di Drag&Drop da esplora risorse e si cerca di spostare uno o più file. Nell'esempio precedente si vuole supportare solamente questo formato, perciò si controlla se è presente.

Nell'implementazione dell'evento Drop, l'operazione è conclusa, e bisogna quindi recuperare il dato e agire di conseguenza secondo la logica dell'applicazione. Nell'esempio seguente per esempio, si crea un menu contestuale per mostrare due azioni da eseguire sul dato:

C#

void ListBox_Drop(object sender, DragEventArgs e)
{
    ContextMenu context = new ContextMenu();
    MenuItem item;

    // Creo i due menu per aggiungere o mostra l'elemento inserito
    item = new MenuItem();
    item.Header = "_Aggiungi";
    item.Tag = e.Data;
    item.Click += new RoutedEventHandler(AddItem_Click);
    context.Items.Add(item);

    item = new MenuItem();
    item.Tag = e.Data;
    item.Click += new RoutedEventHandler(ShowItem_Click);
    item.Header = "_Mostra";
    context.Items.Add(item);

    // Mostra il context menu
    context.IsOpen = true;
}

VB

Sub ListBox_Drop(sender As Object, e As DragEventArgs)
    Dim context As New ContextMenu()
    Dim item As MenuItem

    ' Creo i due menu per aggiungere o mostra l'elemento inserito
    item = New MenuItem()
    item.Header = "_Aggiungi"
    item.Tag = e.Data
    AddHandler(item.Click, New RoutedEventHandler(AddressOf AddItem_Click))
    context.Items.Add(item)

    item = new MenuItem()
    item.Tag = e.Data
    AddHandler(item.Click, New RoutedEventHandler(AddressOf ShowItem_Click))
    item.Header = "_Mostra"
    context.Items.Add(item)

    ' Mostra il context menu
    context.IsOpen = True
End Sub

I gestori dei menu non devono far altro che recuperare il dato passato tramite Tag e interrogarlo. Nel caso di FileDrop si riceve un array di stringhe contenenti il percorso assoluto al file:

C#

void ShowItem_Click(object sender, RoutedEventArgs e)
{
    // Risalgo all'IDataObject contenente le informazioni
    MenuItem item = (MenuItem)sender;
    IDataObject data = (IDataObject)item.Tag;

    // Mostra la lista dei formati che il data object contiene
    ShowDraggedData(data);

    // Mostro una finestra in sostituzione dell'ipotetica effettiva operazione
    string[] files = (string[])data.GetData(DataFormats.FileDrop);
    MessageBox.Show("Mostra file " + files[0]);
}

VB

Sub ShowItem_Click(object sender, RoutedEventArgs e)
    ' Risalgo all'IDataObject contenente le informazioni
    Dim item As MenuItem = DirectCast(sender, MenuItem)
    IDataObject data = DirectCast(item.Tag, IDataObject)

    ' Mostra la lista dei formati che il data object contiene
    ShowDraggedData(data)

    ' Mostro una finestra in sostituzione dell'ipotetica effettiva operazione
    Dim files As String() = DirectCast(data.GetData(DataFormats.FileDrop), String())
    MessageBox.Show("Mostra file " & files(0))
End Sub

Ecco due immagini che mostrano la fase di Drag&Drop e la sua fine quando viene mostrato il menu contestuale:

*

*

 

Team Foundation Server: Sbloccare un file in Lock esclusivo da parte di un altro utente

Di Lorenzo Barbieri - Microsoft MVP

Team Foundation Server permette di mettere Lock sui file in maniera diretta (tramite il comando Lock) o mentre si effettua il Check Out di un file.

A volte capita che un utente metta un Lock su un file, e poi non sia temporaneamente o definitivamente disponibile per sbloccarlo (assenza dell’utente, abbandono del team, mancanza di connettività di rete, etc...). Se c’è la necessità di sbloccare un file e si ha i permessi per farlo, bisogna intervenire da riga di comando. E’ necessario però prima individuare il nome completo del file e il workspace in cui il file risulta bloccato.

Per individuare il nome completo del file si può andare su proprietà:

*

*

Per individuare il Workspace associato al nome del file sul server bisogna utilizzare il comando tf.exe con il parametro status, il nome del server, l’utente che ha il file in Lock:

tf status $percorso/nomefile /server:NOMESERVER /user: UTENTE /format:detailed

Lanciando il comando si ottiene una lista di informazioni relative al file, tra cui il Workspace in cui il file risulta in Lock.

*

Se si vuole avere invece l’elenco completo dei file che hanno un Lock da parte di un utente, ed i relativi Workspace, bisogna usare il comando precedente, senza specificare il nome del file sul server:

tf status /server:NOMESERVER /user: UTENTE /format:detailed /login: UTENTEUNLOCK

Lanciando il comando, in output si ottiene la lista dei file che hanno Lock in sospeso:

*

Una volta individuato il file e il workspace si può togliere il Lock usando il comando tf.exe con il paramentro lock e l’opzione lock:none (è necessario il permesso di rimuovere i Lock di altri utenti):

tf lock $percorso/nomefile /lock:none /workspace:workspace;utente /server:NOMESERVER /login:UTENTEUNLOCK

*

E' anche possibile fare Undo della modifica pendente nel Workspace dell’utente con il comando tf.exe e il parametro undo (se l’utente dispone del permesso di Undo delle modifiche degli altri utenti):

tf undo $percorso/nomefile /workspace:workspace;utente /server:NOMESERVER /login:UTENTEUNDO

*

L’Undo della modifica rimuove anche il Lock eventualmente associato alla modifica.

Nel caso l’utente ha molti file in Lock, e le modifiche pendenti non vanno recuperate, si può cancellare l’intero Workspace (se si dispone del permesso di amministrazione dei Workspace) con il comando:

tf workspace /delete:workspace;utente /server:NOMESERVER /login:UTENTEAMMINISTRATORE

In alternativa ai tool da riga di comando è possibile utilizzare un tool gratuito chiamato Team Foundation Sidekicks (scaricabile da questa pagina in inglese: http://www.attrice.info/cm/tfs/index.htm) che dispone di una serie di funzionalità aggiuntive per Team Foundation Server, tra cui lo Status Sidekick (che permette di gestire i file, togliere i Lock, fare l’Undo, etc...) e il Workspace Sidekick (che permette di gestire i Workspace, vedere quali file contengono, cancellarli, etc...).