Leggi la prima parte qui
Scelgo le ultime due colonne da cui partire per la valorizzazione in quanto sono le più semplici, infatti, ho un valore per ogni titolarità.
Abbiamo bisogno di:
- Creare le colonne Rendita e Valore
- Mappare le colonne con i campi RenditaCatastale e ValoreFabbricato della classe Titolarita
- Formattare il numero come 1.000,00
- Aggiungere una riga per ogni Titolarita
Partiamo dalla grid. L'implementazione attuale è la seguente e non fa ancora nulla:
public class Grid
{
public Cell this[int row, int col] { get { return null; } }
public List<ColumnHeader> ColumnHeaders { get { return null; } }
}
Come griglia userò una ListView alla quale dobbiamo impostare alcune property per farla funzionare come una grid:
- La prima colonna la facciamo invisibile in quanto si comporta diversamente dalle altre come documentato nelle msdn qui. In pratica viene ignorato il text align nella prima colonna.
- Dobbiamo settare la DetailsView
- Dobbiamo visualizzare le righe
- La selezione deve coprire tutte le celle di una riga
- dobbiamo poter aggiungere colonne e celle
Ecco il test per l'inizializzazione della grid:
[Test]
public void ShouldInitializeListView()
{
ListView listView = new ListView();
Grid grid = new Grid(listView);
grid.Initialize();
Assert.That(listView.Columns[0].Width, Is.EqualTo(0));
Assert.That(listView.View, Is.EqualTo(View.Details));
Assert.That(listView.FullRowSelect, Is.True);
Assert.That(listView.GridLines, Is.True);
}
Farlo passare è banale:
public void Initialize()
{
m_listView.View = View.Details;
m_listView.Columns.Add("").Width = 0;
m_listView.FullRowSelect = true;
m_listView.GridLines = true;
}
Aggiungiamo la possibilità di creare colonne:
[Test]
public void ShouldAppendColumnToListView()
{
m_grid.Initialize();
m_grid.AppendColumn("title");
Assert.That(m_listView.Columns[1].Text, Is.EqualTo("title"));
}
Aggiungiamo una riga vuota:
[Test]
public void ShouldAppendRowWithoutCells()
{
m_grid.Initialize();
m_grid.AppendEmptyRow();
Assert.That(m_listView.Items.Count, Is.EqualTo(1));
}
Ed ora una cella:
[Test]
public void ShouldAppendCellToTheLastRow()
{
m_grid.Initialize();
m_grid.AppendEmptyRow();
m_grid.AppendCell("some text");
Assert.That(m_listView.Items[0].SubItems[1].Text, Is.EqualTo("some text"));
}
A questo punto abbiamo bisogno di un data component processor che aggiunga una riga per ogni titolarità:
[Test]
public void ShouldAppendRowToTheGrid()
{
ListView listView = new ListView();
var processor = new RowAppenderComponentProcessor<Titolarita>(new Grid(listView));
processor.Process(new Titolarita());
Assert.That(listView.Items.Count, Is.EqualTo(1));
}
Formattiamo il numero decimale:
[Test]
public void ShouldFomatValue()
{
Money money = new Money(1234.56M);
string formatted = money.Format();
Assert.That(formatted, Is.EqualTo("1.234,56"));
}
Ci siamo quasi, manca ora un oggetto che prenda il valore del campo e lo formatti:
[Test]
public void ShouldAppendCellFormattingComponentValue()
{
ListView listView = new ListView();
Grid grid = new Grid(listView);
grid.AppendEmptyRow();
var processor = new CellAppenderComponentProcessor<Titolarita, Money>(
titolarita => new Money(titolarita.RenditaCatastale),
grid);
processor.Process(new Titolarita {RenditaCatastale = 1000M});
Assert.That(listView.Items[0].SubItems[1].Text, Is.EqualTo("1.000,00"));
}
Di questo riporto anche l'implementazione perchè forse non risulta ovvia:
public class CellAppenderComponentProcessor<TComponent, TData> : IDataComponentProcessor<TComponent>
{
private readonly Func<TComponent, TData> m_getValueFunc;
private readonly Grid m_grid;
public CellAppenderComponentProcessor(Func<TComponent, TData> func, Grid grid)
{
m_getValueFunc = func;
m_grid = grid;
}
public void Process(TComponent dataComponet)
{
TData data = m_getValueFunc.Invoke(dataComponet);
m_grid.AppendCell(data.ToString());
}
}
Ora è il momento di mettere tutti i pezzi insieme:
Grid grid = new Grid(m_fabbricatiListView);
grid.Initialize();
grid.AppendColumn("Rendita catastale");
grid.AppendColumn("Valore fabbricato");
var praticaProcessor = new OneToManyDataComponentRelation<Pratica, Fabbricato>(
pratica => pratica.Fabbricati,
new OneToManyDataComponentRelation<Fabbricato, Titolarita>(
fabbricato => fabbricato.Titolarita,
new MultiComponentProcessor<Titolarita>(
new RowAppenderComponentProcessor<Titolarita>(grid),
new CellAppenderComponentProcessor<Titolarita, Money>(
titolarita => new Money(titolarita.RenditaCatastale), grid),
new CellAppenderComponentProcessor<Titolarita, Money>(
titolarita => new Money(titolarita.ValoreFabbricato), grid))));
Avendo la necessità eseguire più Processor ho creato la classe MultiComponentProcessor:
public class MultiComponentProcessor<T> : IDataComponentProcessor<T>
{
private readonly IDataComponentProcessor<T>[] m_processors;
public MultiComponentProcessor(params IDataComponentProcessor<T>[] processors)
{
m_processors = processors;
}
public void Process(T dataComponet)
{
foreach (var processor in m_processors)
{
processor.Process(dataComponet);
}
}
}
La costruzione degli oggetti che ho scritto sopra non è forse leggibilissima, ma il problema si può risolvere facilmente creando un oggetto Builder con una fluent interface.
Devo riempire una grid con i seguenti dati:
La quale rappresenta un esempio di una lista di fabbricati di una pratica ICI. Ogni riga rappresenta la titolarità di un fabbricato quindi sono rappresentati tre fabbricati di cui il primo con due titolarita.
Come vediamo la rappresentazione dei dati ha un pò di logica ad esempio la prima colonna ha due formattazioni diverse in base ad un valore booleano, poi dalla seconda colonna i dati legati al fabbricato vengono ripetuti se ci sono diverse titolarità ed infine ci sono alcuni importi da visualizzare.
Vediamo la parte di dominio legata ai dati della lista:
Come si vede abbiamo due relazioni uno a molti: Pratica -> Fabbricati e Fabbricato -> Titolarita.
Partiamo dal setup del test di accettazione (per chi non lo sapesse un test di accettazione è un test end to end, cioè testa la feature con tutti i componenti):
[SetUp]
public void SetUp()
{
Pratica praticaIci = new Pratica();
Fabbricato fabbricatoMilano = new Fabbricato
{
Verificato = true,
Progressivo = 1,
Comune = "Milano",
Indirizzo = "Viale Roma"
};
fabbricatoMilano.Titolarita.Add(new Titolarita{ RenditaCatastale = 180.76M, ValoreFabbricato = 18980.70M });
fabbricatoMilano.Titolarita.Add(new Titolarita{ RenditaCatastale = 93.02M, ValoreFabbricato = 8950.20M });
praticaIci.Fabbricati.Add(fabbricatoMilano);
...
}
Ho messo solo il primo per leggibilità. Vediamo il codice del test ora:
[Test]
public void ShouldFillFabbricatiGrid()
{
Grid grid = new Grid();
FabbricatiGridFiller filler = new FabbricatiGridFiller(grid);
filler.Fill(praticaIci);
Assert.That(grid.ColumnHeaders[0].Text, Is.EqualTo("Verif."));
Assert.That(grid[0, 0].Text, Is.EqualTo("SI"));
Assert.That(grid[0, 0].BackColor, Is.EqualTo(Color.LightGreen));
Assert.That(grid[0, 0].FontStyle, Is.EqualTo(FontStyle.Bold));
Assert.That(grid[1, 0].Text, Is.Empty);
Assert.That(grid.ColumnHeaders[1].Text, Is.EqualTo("Progr."));
Assert.That(grid[0, 1].Text, Is.EqualTo("1"));
Assert.That(grid[1, 1].Text, Is.EqualTo("1"));
}
Ho riportato solo una parte delle Assert, ma il concetto spero sia chiaro.
A questo punto facciamo compilare il codice del test creando delle classi senza implementazione, che non riporto.
Il motivo per cui sono partito dal test di accettazione sta nel fatto che mi mi
chiarisce l'obiettivo a cui devo arrivare, ma ora utilizzerò il TDD per arrivare all'implementazione della feature. Quindi non preoccupatevi in questo test non ho usato nessun
Big Design Up Front (BDUF).
Qual è il prossimo passo?
Per rispondere a questa domanda definiamo il principio che vogliamo che il nostro design debba rispettare. Nel mio caso specifico ho molte liste da riempire simili a questa i cui dati sono memorizzati in classi analoghe a Fabbricato, Titolarita ecc. Quindi voglio descrivere il binding dei dati senza dipendere dai dati stessi. Inoltre non voglio utilizzare la reflection, ma un grafo di oggetti che collaborino tra loro per ottenere il risultato.
Partirò dalla relazione tra gli oggetti che contengono i dati (Pratica, Fabbricato, Titolarita). Come si può vedere sono due relazioni uno a molti la prima tra Pratica e Fabbricato, la seconda tra Fabbricato e Titolarita.
Come chiamiamo questo oggetto? OneToManyDataComponentRelation. Gli oggetti come Fabbricato li ho battezzati DataComponent e sono oggetti solitamente mappati con una tabella e che mi arrivano da un ORM di vostra scelta.
Vediamo il test:
[Test]
public void ShouldProcessRelatedComponents()
{
var componentProcessor = this;
var relation = new OneToManyDataComponentRelation<Pratica, Fabbricato>(
pratica => pratica.Fabbricati, componentProcessor);
relation.ProcessRelatedComponents(praticaIci);
Assert.That(componentProcessedCount, Is.EqualTo(2));
}
Se vediamo il codice della classe penso che risulterà più semplice la comprensione del test:
public class OneToManyDataComponentRelation<TOne, TMany>
{
private readonly Func<TOne, IEnumerable<TMany>> m_getRelatedComponentsFunc;
private readonly IDataComponentProcessor<TMany> m_relatedComponentProcessor;
public OneToManyDataComponentRelation(
Func<TOne, IEnumerable<TMany>> getRelatedComponentsFunc,
IDataComponentProcessor<TMany> relatedComponentProcessor)
{
m_getRelatedComponentsFunc = getRelatedComponentsFunc;
m_relatedComponentProcessor = relatedComponentProcessor;
}
public void ProcessRelatedComponents(TOne dataComponent)
{
IEnumerable<TMany> components = m_getRelatedComponentsFunc.Invoke(dataComponent);
foreach (var component in components)
{
m_relatedComponentProcessor.Process(component);
}
}
}
In pratica ho definito una relazione uno a molti tra due tipi in questo Caso Pratica e Fabbricato. E per ogni link della relazione viene eseguito il metodo process dell'interfaccia IDataComponent.
Un'altra cosa interessante è l'inizializzazione del data processor a this e cioè alla classe di test. Questa tecnica si chiama Self Shunt ed è utile quando il test di interazione è molto semplice. Vediamo la classe di test al completo:
[TestFixture]
public class OneToManyDataComponentRelationTests : IDataComponentProcessor<Fabbricato>
{
private int componentProcessedCount;
private Pratica praticaIci;
[SetUp]
public void SetUp()
{
componentProcessedCount = 0;
praticaIci = new Pratica();
praticaIci.Fabbricati.Add(new Fabbricato());
praticaIci.Fabbricati.Add(new Fabbricato());
}
[Test]
public void ShouldProcessRelatedComponents()
{
var componentProcessor = this;
var relation = new OneToManyDataComponentRelation<Pratica, Fabbricato>(
pratica => pratica.Fabbricati, componentProcessor);
relation.ProcessRelatedComponents(praticaIci);
Assert.That(componentProcessedCount, Is.EqualTo(2));
}
void IDataComponentProcessor<Fabbricato>.Process(Fabbricato dataComponet)
{
componentProcessedCount++;
}
}
Ora una cosa che mi piacerebbe è di poter concatenare le relazioni.
new OneToManyDataComponentRelation<Pratica, Fabbricato>(
pratica => pratica.Fabbricati,
new OneToManyDataComponentRelation<Fabbricato, Titolarita>(
fabbricato => fabbricato.Titolarita,
...));
Per ottenere questo risultato basta semplicemente far implementare l'interfaccia IDataComponentProcessor alla nostra relation:
public class OneToManyDataComponentRelation<TOne, TMany> : IDataComponentProcessor<TOne>
{
private readonly Func<TOne, IEnumerable<TMany>> m_getRelatedComponentsFunc;
private readonly IDataComponentProcessor<TMany> m_relatedComponentProcessor;
public OneToManyDataComponentRelation(
Func<TOne, IEnumerable<TMany>> getRelatedComponentsFunc,
IDataComponentProcessor<TMany> relatedComponentProcessor)
{
m_getRelatedComponentsFunc = getRelatedComponentsFunc;
m_relatedComponentProcessor = relatedComponentProcessor;
}
public void Process(TOne dataComponent)
{
IEnumerable<TMany> components = m_getRelatedComponentsFunc.Invoke(dataComponent);
foreach (var component in components)
{
m_relatedComponentProcessor.Process(component);
}
}
}
Scriverò un altro post per la parte rimanente.