Comment ajouter du contenu à un calendrier Silverlight

Bonjour,

Depuis quelques temps je cherchais à enrichir le contrôle Calendar de Silverlight afin de pouvoir afficher des données à partir d’une base.

Par défaut, ce contrôle ne sert qu'à proposer à un utilisateur de choisir une date sur un calendrier, il ne permet pas de parcourir la collection des jours du calendrier au runtime afin d'en modifier le contenu.

Partons sur un exemple concret : j'ai en mémoire un ensemble de rendez-vous (ici il s'agit d'objets créés en mémoire, dans un scénario réel on irait chercher ces données dans une base à l'aide d'un service), et je souhaite afficher un bouton pour chaque rendez-vous dans le calendrier.

Voici comment faire pour arriver à nos fins.

Pour commencer à définir l'aspect du contrôle Calendar, la première chose à faire est d'éditer le template du Calendar (à l'aide d'Expression Blend par exemple), puis de modifier le XAML pour changer le template du Calendar.

Notez que les espaces de noms suivants doivent être importés :
xmlns:System_Windows_Controls_Primitives="clr-namespace:System.Windows.Controls.Primitives;assembly=System.Windows.Controls"
xmlns:basics="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"

Dans l'exemple suivant, j'ai modifié la couleur de fond afin de lui donner une couleur unie car la modification de la hauteur des cellules du calendrier au runtime ne me permettait pas de garder la couleur de fond prédéfinie par le contrôle (je vous laisse faire l’essai). J’ai laissé les autres valeurs par défaut…

<Style x:Key="CalendarStyle1" TargetType="basics:Calendar">
  <Setter Property="IsTabStop" Value="False"/>
  <Setter Property="Background" Value="#FFFFFFFF" />
  <Setter Property="BorderBrush">
    <Setter.Value>
      <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
        <GradientStop Color="#FFA3AEB9" Offset="0"/>
        <GradientStop Color="#FF8399A9" Offset="0.375"/>
        <GradientStop Color="#FF718597" Offset="0.375"/>
        <GradientStop Color="#FF617584" Offset="1"/>
      </LinearGradientBrush>
    </Setter.Value>
  </Setter>
  <Setter Property="BorderThickness" Value="1"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="basics:Calendar">
        <StackPanel HorizontalAlignment="Center" x:Name="Root">

          <System_Windows_Controls_Primitives:CalendarItem
            Background="{TemplateBinding Background}"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}"
            x:Name="CalendarItem"/>
        </StackPanel>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

Jusqu'ici je n'ai fait que modifier la couleur de fond du calendrier, toutefois libre à vous de modifier le template du Calendar à votre guise.

Ce qui m'intéresse plus particulièrement ici, c'est de modifier l'objet CalendarDayButton afin d'y redéfinir le Template qui affichera le contenu de chaque jour (de type CalendarDayButton).
Voici comment je l'ai implémenté :

<Style x:Key="CalendarDayButtonStyle"
TargetType="System_Windows_Controls_Primitives:CalendarDayButton">
  <Setter Property="Width" Value="80"/>
  <Setter Property="Template">
    <Setter.Value>

      <ControlTemplate 
TargetType="System_Windows_Controls_Primitives:CalendarDayButton">
              < Border BorderBrush="#FF598788"
BorderThickness="1,1,1,1"
CornerRadius="2,2,2,2">
<Border.Background>

<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">  

<GradientStop Color="#FFD3DEE8" Offset="0"/>  
             <GradientStop Color="#FFFFFFFF" Offset="1"/>  
          </LinearGradientBrush>  
         </Border.Background>
         <StackPanel HorizontalAlignment="Center">
           <System_Windows_Controls_Primitives:CalendarDayButton
             Loaded="CalendarDayButton_Loaded"
             Background="{TemplateBinding Background}"
             BorderBrush="{TemplateBinding BorderBrush}"
             Content="{TemplateBinding Content}"
             BorderThickness="{TemplateBinding BorderThickness}"
             x:Name="CalendarDayButton"/>
          </StackPanel>
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

La première modification faite ici, c'est le changement de la propriété Width de l'objet CalendarDayButton
Le fait de modifier la largeur de chaque bouton me permet notamment de modifier la largeur globale du calendrier (vous aviez peut-être déjà remarqué que le fait de changer la propriété Width du Calendar lui-même ne fait rien ??).

Notez qu’après avoir modifié cette propriété, si vous passez l’affichage du calendrier en mode Month ou Decade, le calendrier retrouvera sa largeur d’origine. Pour éviter cela, vous pouvez modifier le style de l’objet CalendarButton pour changer la largeur des boutons.
Afin que la largeur globale du calendrier ne change pas, définissez la largeur d’un CalendarButton égale à : 7 x LargeurDuCalendarDayButton / 4, car en mode Month ou Decade, il n’y a que quatre boutons dans la largeur du calendrier (voir dans le code source de l’exemple téléchargeable en fin de ce post).

La modification principale, qui me permettra d'ajouter du contenu au contrôle se fait sur la définition du Template de l'objet CalendarDayButton.
Notez que j'ai modifié ce template afin d'y ajouter une bordure (objet Border sur lequel j'ai ajouté un dégradé en couleur de fond, ainsi que des angles arrondis), ainsi qu'un objet StackPanel à l'intérieur de cette bordure. Ce StackPanel me permettra d'empiler des objets sous le CalendarDayButton existant, qui se charge d'afficher le jour dans le calendrier.

La dernière modification à ce template est la suivante : je m'abonne à l'événement Loaded de l'objet CalendarDayButton.
Cet événement va être levé une fois par CalendarDayButton (soit 42 fois au total car le calendrier affiche 42 jours), et ne sera plus levé par la suite, même lorsque l'on change de mois.
L'idée est donc de stocker les références des 42 CalendarDayButton en mémoire, afin de pouvoir les manipuler par la suite.

private List<CalendarDayButton> CalendarButtons;

//Création de la liste des CalendarDayButton
private void CalendarDayButton_Loaded(object sender, RoutedEventArgs e)
{
CalendarDayButton cdb = sender as CalendarDayButton;
CalendarButtons.Add(cdb);

//Le calendrier comporte 42 jours
//après que le dernier jour soit chargé, on remplit
//le calendrier avec les données
if (CalendarButtons.Count == 42)
RemplirCalendrier(
new DateTime(Calendar1.DisplayDate.Year,
Calendar1.DisplayDate.Month, 1));
}

Une fois les contrôles CalendarDayButton stockés, nous allons pouvoir les manipuler pour afficher du contenu dedans.
Lors du chargement du dernier jour, on appelle la méthode RemplirCalendrier (définie plus loin dans cet article) en lui passant le premier jour du mois courant en paramètre afin d'afficher les rendez-vous du mois courant.

Avant toute autre chose, nous allons charger des données.
Pour cela, définissons une classe RDV :

class RDV
{
public DateTime date { get; set; }
public string intitule { get; set; }
}

Puis dans l'événement Loaded du contrôle Silverlight, on crée une liste de RDV, et on définit quelques paramètres du calendrier (afin que la semaine commence un lundi par exemple) :

List<RDV> mesRDVs;

private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
{
Calendar1.SelectionMode = CalendarSelectionMode.None;
Calendar1.FirstDayOfWeek = DayOfWeek.Monday;
CalendarButtons = new List<CalendarDayButton>();

mesRDVs = new List<RDV>
{

new RDV { date=new DateTime(2008, 11, 1), intitule="rdv1" },
new RDV { date=new DateTime(2008, 11, 8), intitule="rdv2" },
new RDV { date=new DateTime(2008, 11, 8), intitule="rdv3" },
new RDV { date=new DateTime(2008, 11, 25), intitule="rdv4" },
new RDV { date=new DateTime(2008, 12, 7), intitule= "rdv5" },
new RDV { date=new DateTime(2008, 10, 28), intitule= "rdv6" }
};
}

Maintenant que nous avons chargé des données il faut que l'on puisse savoir à quel jour du calendrier correspond chaque CalendarDayButton de notre liste lorsqu'on change de mois sur le calendrier.

//Changement de mois
private void Calendar1_DisplayDateChanged(object sender, CalendarDateChangedEventArgs e)
{

RemplirCalendrier(Calendar1.DisplayDate);
}

Ceci sera fait dans la méthode RemplirCalendrier ci-dessous.

private void RemplirCalendrier(DateTime PremierJourDuMoisCourant)
{
int compteur = 0;
StackPanel conteneur;
DateTime jourCourant;

int JourDu1erDuMois = (int)PremierJourDuMoisCourant.DayOfWeek;

//Cas du dimanche
if (JourDu1erDuMois == 0) JourDu1erDuMois = 7;
//Cas du lundi -> Si le Premier du mois est un lundi,
//alors il sera affiché sur la 2ème ligne du calendrier
if (JourDu1erDuMois == 1) JourDu1erDuMois = 8;

//Parcours de tous les jours

foreach (CalendarDayButton cdb in CalendarButtons)
{
conteneur = cdb.Parent as StackPanel;
jourCourant = PremierJourDuMoisCourant.AddDays(compteur).AddDays(1 - JourDu1erDuMois);

int nbControles = conteneur.Children.Count;

//Suppression du contenu du jour
//sauf du contrôle CalendarDayButton
for (int i = nbControles - 1; i > 0; i—)
conteneur.Children.RemoveAt(i);

//Sélection des RDV du jour courant
var query = from rdv in mesRDVs
where rdv.date.Date == jourCourant
select new { rdv.intitule };

//Ajout d'un contrôle Button
//pour chaque RDV
foreach (var rdv in query)
{
Button btn = new Button();
btn.Content = rdv.intitule;
conteneur.Children.Add(btn);
}

compteur ++;
}

}

La première étape est de calculer quel est le jour du 1er du mois courant.
A partir de cette information, on peut parcourir la collection de CalendarDayButton et définir la date que chaque CalendarDayButton représente.
Notez qu'il y a un un cas particulier pour le lundi (si je détermine que le lundi est le premier jour de la semaine) : si le premier du mois tombe un lundi, alors le premier du mois sera affiché sur la deuxième ligne du calendrier, afin que l'on puisse voir quelques jours du mois précédent sur le calendrier. Si vous définissez un autre jour comme étant le début de semaine, alors il faudra adapter un peu le code…

Il faut donc parcourir la liste de CalendarDayButton.
Pour chaque CalendarDayButton, je récupère son parent (qui est le StackPanel définit dans le template).
A partir de ce StackPanel, je supprime tous les contrôles enfants (sauf le CalendarDayButton lui-même que j'ai laissé à l'indice 0), afin de supprimer tous les RDV précédemment placés dans ce StackPanel.
Je définis le jour courant, puis je fais une requête sur ma collection de RDV afin de récupérer tous les RDV du jour courant.
Je boucle enfin sur le résultat obtenu, et j'ajoute pour chaque RDV un bouton (on pourrait imaginer autre chose) au StackPanel.
J'aurai aussi pu gérer l'événement click de chaque bouton afin d'afficher le détail du RDV lorsque l'utilisateur clique sur un RDV par exemple, mais je vous laisse le soin d'arranger tout cela comme vous le souhaitez...

Le résultat obtenu est le suivant :

image

Si vous souhaitez changer le style général de votre calendrier, alors vous pouvez vous rendre sur le blog de Corrina Barber : https://blogs.msdn.com/corrinab/.

Vous pouvez télécharger le code source de cet exemple ici.

A bientôt,
Aurélien