This post introduces you to how to extend the behavior of the graph without modifying the source code. This topic uses the attached properties to extend the behavior. More information on attached properties can be found on MSDN.
In this post, we will extend the graph to show data view.
Before we get started on extending the behavior, create a new Silverlight project using Microsoft Expression Blend called "ExtendingGraphBehavior" and create a local copy of the control template. Refer to this section in a previous post for assistance.
After you have generated a local copy of the template, we need to add two classes to the project. Since Blend doesn't support adding class files into the project, open the project in Microsoft Visual Studio and add two classes named "DataView" and "TimeGraphExtender" to the project.
DataView class will contain templates for button and dataview elements. TimeGraphExtender class will contain attached properties and also handles the button clicked event.
First let us create the DataView class.
Add two DataTemplate properties named "ButtonTemplate" and "DataViewTemplate" to the class. After you add the properties, the DataView class should look like the following:
public class DataView
{
public DataTemplate ButtonTemplate
{
get;
set;
}
public DataTemplate DataViewTemplate
{
get;
set;
}
} |
Now add attached properties to the TimeGraphExtender class. We will need the following attached properties
- DataViewProperty – This property will be of type DataView which we have just created.
- DataViewButtonProperty – This property will be of type Button and will hold the Button element defined in the DataView.
- DataViewElementProperty – This property will be of type DataGrid and will hold the DataViewElement defined in the DataView.
Once attached properties are defined, add a property changed call back to DataView property. In this call back method, we will load the DataView templates. The code should look like this:
public class TimeGraphExtender
{
// Define attached properties
public static readonly DependencyProperty DataViewProperty = DependencyProperty.RegisterAttached("DataView", typeof(DataView), typeof(TimeGraphExtender), new PropertyMetadata(new PropertyChangedCallback(OnDataViewPropertyChanged)));
private static readonly DependencyProperty DataViewElementProperty = DependencyProperty.RegisterAttached("DataViewElement",typeof(DataGrid), typeof(TimeGraphExtender), null);
private static readonly DependencyProperty DataViewButtonProperty = DependencyProperty.RegisterAttached("DataViewButton", typeof(Button), typeof(TimeGraphExtender), null);
// Define CLR properties for the above attached properties
public static DataView GetDataView(DependencyObject obj)
{
return (DataView)obj.GetValue(DataViewProperty);
}
public static void SetDataView(DependencyObject obj, DataView value)
{
obj.SetValue(DataViewProperty, value);
}
private static DataGrid GetDataViewElement(DependencyObject obj)
{
return (DataGrid)obj.GetValue(DataViewElementProperty);
}
private static void SetDataViewElement(DependencyObject obj, DataGrid value)
{
obj.SetValue(DataViewElementProperty, value);
}
private static Button GetDataViewButton(DependencyObject obj)
{
return (Button)obj.GetValue(DataViewButtonProperty);
}
private static void SetDataViewButton(DependencyObject obj, Button value)
{
obj.SetValue(DataViewButtonProperty, value);
}
// Property changed event handler
private static void OnDataViewPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
DataView dataView = e.NewValue as DataView;
Button button = dataView.ButtonTemplate.LoadContent() as Button;
DataGrid dataGrid = dataView.DataViewTemplate.LoadContent() as DataGrid;
if (button != null && dataGrid != null)
{
}
}
}
} |
Now that we have defined DataView and TimeGraphExtender, we need to add elements in DataView to a graph and specify a view for the TimeGraphExtender.
Before we add elements to graph, let us define them. To do that select the Control that has the control template defined and switch to XAML view.
- Define an xmlns with a namespace prefix of "local"
| xmlns:local="clr-namespace:ExtendingGraphBehavior" |
- Add the following style before the TimeLineGraph template
<local:DataView x:Key="DataView">
<local:DataView.ButtonTemplate>
<DataTemplate>
<Button Content="T" Width="25"/>
</DataTemplate>
</local:DataView.ButtonTemplate>
<local:DataView.DataViewTemplate>
<DataTemplate>
<extended:DataGrid AutoGenerateColumns="False" Grid.Row="2" Grid.RowSpan="2" Grid.Column="0" Grid.ColumnSpan="2" Canvas.ZIndex="11" Visibility="Collapsed" >
<extended:DataGrid.Columns>
<extended:DataGridTextColumn Header="Date" Binding="{Binding Value}" IsReadOnly="True" CanUserSort="False" />
<extended:DataGridTextColumn Header="Value" Binding="{Binding}" IsReadOnly="True" CanUserSort="False" />
</extended:DataGrid.Columns>
</extended:DataGrid>
</DataTemplate>
</local:DataView.DataViewTemplate>
</local:DataView> |
Note: DataGrid resides in System.Windows.Controls.Data.dll. We need to add a reference to the DLL and define an xmlns with a namespace prefix of "extended"
xmlns:extended="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" |
In order for all three buttons to be completely visible in the title area, we need to increase the width of the column containing the buttons.
Find element name "ELEMENT_TitleArea" in the template and increase the width of third column from "80" to "100".
<Grid x:Name="ELEMENT_TitleArea" Grid.ColumnSpan="2" Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5*"/>
<ColumnDefinition Width="5*"/>
<ColumnDefinition Width="100"/>
|
Now we have the attached properties and templates defined, let us assign the template to behavior and attach the behavior to the graph. To do this find element name "ELEMENT_layoutRoot" in the template and add the attached property.
| <Grid x:Name="ELEMENT_layoutRoot" Background="{TemplateBinding Background}" local:TimeGraphExtender.DataView="{StaticResource DataView}"> |
After adding the attached property to the graph, we need to add the UI elements to the view. We also need to handle the click event for the button and toggle the visibility of the new DataView.
Modify the TimeGraphExtender class and add the following code.
public class TimeGraphExtender
{
// Define attached properties
public static readonly DependencyProperty DataViewProperty = DependencyProperty.RegisterAttached("DataView", typeof(DataView), typeof(TimeGraphExtender), new PropertyMetadata(new PropertyChangedCallback(OnDataViewPropertyChanged)));
private static readonly DependencyProperty DataViewElementProperty = DependencyProperty.RegisterAttached("DataViewElement",typeof(DataGrid), typeof(TimeGraphExtender), null);
private static readonly DependencyProperty DataViewButtonProperty = DependencyProperty.RegisterAttached("DataViewButton", typeof(Button), typeof(TimeGraphExtender), null);
// Define CLR properties for the above attached properties
public static DataView GetDataView(DependencyObject obj)
{
return (DataView)obj.GetValue(DataViewProperty);
}
public static void SetDataView(DependencyObject obj, DataView value)
{
obj.SetValue(DataViewProperty, value);
}
private static DataGrid GetDataViewElement(DependencyObject obj)
{
return (DataGrid)obj.GetValue(DataViewElementProperty);
}
private static void SetDataViewElement(DependencyObject obj, DataGrid value)
{
obj.SetValue(DataViewElementProperty, value);
}
private static Button GetDataViewButton(DependencyObject obj)
{
return (Button)obj.GetValue(DataViewButtonProperty);
}
private static void SetDataViewButton(DependencyObject obj, Button value)
{
obj.SetValue(DataViewButtonProperty, value);
}
// Property changed event handler
private static void OnDataViewPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Grid layoutRoot = d as Grid;
if (layoutRoot != null && layoutRoot.Name == "ELEMENT_layoutRoot")
{
if (e.NewValue != null)
{
DataView dataView = e.NewValue as DataView;
Button button = dataView.ButtonTemplate.LoadContent() as Button;
DataGrid dataGrid = dataView.DataViewTemplate.LoadContent() as DataGrid;
if (button != null && dataGrid != null)
{
layoutRoot.Loaded += new RoutedEventHandler(LayoutRoot_Loaded);
TimeGraphExtender.SetDataViewButton(layoutRoot, button);
TimeGraphExtender.SetDataViewElement(layoutRoot, dataGrid);
}
}
}
}
private static void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
{
Grid layoutRoot = sender as Grid;
if (layoutRoot != null)
{
FrameworkElement element = layoutRoot.FindName("ELEMENT_ScaleToFit") as FrameworkElement;
if (element != null && element.Parent is StackPanel)
{
Button button = TimeGraphExtender.GetDataViewButton(layoutRoot);
if (button != null)
{
(element.Parent as StackPanel).Children.Add(button);
button.Click += new RoutedEventHandler(Button_Click);
}
DataGrid dataGrid = TimeGraphExtender.GetDataViewElement(layoutRoot);
if (dataGrid != null)
{
dataGrid.ItemsSource = layoutRoot.DataContext as IEnumerable;
layoutRoot.Children.Add(dataGrid);
}
TimeGraphExtender.SetDataViewElement(button, dataGrid);
}
}
}
private static void Button_Click(object sender, RoutedEventArgs e)
{
DataGrid dataViewElement = TimeGraphExtender.GetDataViewElement(sender as DependencyObject);
if (dataViewElement != null)
{
dataViewElement.Visibility = dataViewElement.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
}
}
} |
Switch to the design view in Blend and you can see a button "T" at the top right corner of the graph:
Give some data to the graph and run the solution. It will look like:
To ensure compliance with the Microsoft Health CUI Design Guidance – Date Display,
we need to display dates in the dd-MMM-yyyy HH:mm:ss format. To do this we need to create a custom IValueConverter and link the converter while binding.
Create a new class using Visual Studio called DateFormatConverter:
public class DateFormatConverter : IValueConverter
{
public object IValueConverter.Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value != null && parameter != null)
{
DateTime dateTime;
bool parseSuccessful = DateTime.TryParse(value as string, CultureInfo.CurrentCulture, DateTimeStyles.AllowWhiteSpaces, out dateTime);
if (parseSuccessful)
{
return dateTime.ToString(parameter as string, CultureInfo.CurrentCulture);
}
}
return null;
}
public object IValueConverter.ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
|
Next, link the newly created converter while binding the date value.
- Create an instance of the DateFormatConverter class. Add the following code above the DataView style:
| <local:DateFormatConverter x:Key="DateFormatConverter" /> |
- Change the first column in DataView template to the following:
|
<extended:DataGridTextColumn Header="Date" Binding="{Binding Value, Converter={StaticResource DateFormatConverter}, ConverterParameter='dd-MMM-yyyy HH:mm:ss'}" IsReadOnly="True" CanUserSort="False" /> |
Run the solution and it will look like:
Now that we have done this for a standalone graph, extend the concept to a GraphHost and the output will look like: