Windows 8 Store Apps: Creating Chart UI using 2D Graphics with C# and XAML

Posted by: Mahesh Sabnis , on 6/20/2013, in Category Windows Store Apps
Views: 16958
Abstract: The 2D graphics API in WinRT can be used easily using C# and XAML to develop charts on Window 8 devices for the Windows Store

Recently, I was discussing Line Of Business (LoB) application development for Modern UI on Windows 8 with one of my clients. One of their requirements was generating Charts based upon the values received from external services like WCF, WEB API etc. That got me going and I decided to implement a sample using C# and XAML.

Windows Store Apps developed using C# and XAML can leverage the 2D Graphics related APIs for drawing - like generating Point collection and drawing lines onscreen using these. Today we will see how we can leverage these APIs

If you are familiar with WPF or Silverlight, then you will find use of these classes really simple with slight variations in WinRT.

 

The WinRT Application

Since WinRT Apps cannot access SQL Databases directly we’ll setup a WCF Service for the Data Access and connect to the service from the WinRT App.

The WCF Service

For this article I have used a SQL Server Database called ‘Company’ with the following Schema:

 database-schema

Task 1: Open VS2012 Pro or (VS Web Developer Express if you are using the Express Editions) and create a new Blank solution, name it as ‘Store_CS_LineChart_StoreApp’. In this solution add a new WCF Service Application. Name it as ‘WCF_SalesService’. Raname ‘IService1.cs’ to ‘IService.cs’ and Service1.svc to Service.svc. Right click on the Service.svc and select ‘View Markup’, rename the Service attribute value to Service from Service1.

Task 2: In this WCF project, add a new ADO.NET EF, complete the Wizard by selecting the above two created tables from the Sql Server. After completing the wizard the mapping will be as below:

ef-relationships

Task 3: Replace the code IService.cs with the following Interface:

using System.ServiceModel;

namespace WCF_SalesService
{
[ServiceContract]
public interface IService
{
  [OperationContract]
  CompanyMaster[] GetCompanies();
  [OperationContract]
  CompanywiseSale[] GetSalesDetails();
  [OperationContract]
  CompanywiseSale[] GetSalesDetailsByCompanyId(int id);
}
}

Task 4: The implementation of the Service Contract is as follows. It uses EntityFramework to retrieve the list of Companies and the Associated Sales Figures:

using System.Linq;

namespace WCF_SalesService
{
public class Service : IService
{
  CompanyEntities objContext;
  public Service()
  {
   objContext = new CompanyEntities();
  }
  /// <summary>
  /// Method to return all Companies
  /// </summary>
  /// <returns></returns>
  public CompanyMaster[] GetCompanies()
  {
   return objContext.CompanyMasters.ToArray();
  }

  /// <summary>
  /// Method to return sales details of all companies
  /// </summary>
  /// <returns></returns>
  public CompanywiseSale[] GetSalesDetails()
  {
   return objContext.CompanywiseSales.ToArray();
  }

  /// <summary>
  /// Method to Sales for the specific company based upon the CompanyID
  /// </summary>
  /// <param name="id"></param>
  /// <returns></returns>
  public CompanywiseSale[] GetSalesDetailsByCompanyId(int id)
  {
   var CompanywiseSales = from sales in objContext.CompanywiseSales
    where sales.CompanyID == id
    select sales;
   return CompanywiseSales.ToArray();
  }
}
}

Task 5: Publish the WCF service on the Web Server. Note here we are not making any changes in Web.Config file. We will using the default basicHttpBinding.

Note: You can also make use of Web API service instead of a WCF service.

The Windows 8 Store App

Task 6: If you are using Visual Studio Pro or above, add a new Windows Store Apps using C#. If you are using the Express editions, start the Visual Studio Express for Windows 8 and create a new Windows Store App using C#. Name it as ‘Store_CS_LineChart’. Add a Service reference to the WCF service published above.

Task 7: In this project, add a new class file. Name it as ‘ModelClass.cs’. Add the following code in it:

using System.Threading.Tasks;
using Store_CS_LineChart.MyRef;
using System.Collections.ObjectModel;

namespace Store_CS_LineChart
{
/// <summary>
/// The mode class used  Make call to WCF Service Proxy
/// and Get companies and sales data.
/// All these methods makes an Async call to WCF Service
/// </summary>
public class ModelClass
{
  ServiceClient Proxy;
  ObservableCollection<CompanyMaster> _ListCompanies;
 
  public ObservableCollection<CompanyMaster> ListCompanies
  {
   get { return _ListCompanies; }
   set { _ListCompanies = value; }
  }
  ObservableCollection<CompanywiseSale> _ListSalesEndingQty;

  public ObservableCollection<CompanywiseSale> ListSalesEndingQty
  {
   get { return _ListSalesEndingQty; }
   set { _ListSalesEndingQty = value; }
  }
  public ModelClass()
  {
   Proxy = new ServiceClient();
   ListCompanies = new ObservableCollection<CompanyMaster>();
   ListSalesEndingQty = new ObservableCollection<CompanywiseSale>();
  }
  /// <summary>
  /// Get Company Names
  /// </summary>
  /// <returns></returns>
  public async Task GetCompanyNames()
  {
   var Companies = await Proxy.GetCompaniesAsync();
   foreach (var item in Companies)
   {
    ListCompanies.Add(item);
   }
  }
 
  /// <summary>
  /// Get Sales Data based upon the CopmanyID
  /// </summary>
  /// <returns></returns>
  public async Task LoadSalesData(int id)
  {
   //Load the Data from the service
   var CompanywiseSales = await Proxy.GetSalesDetailsByCompanyIdAsync(id);
   foreach (var item in CompanywiseSales)
   {
    ListSalesEndingQty.Add(item);
   }
  }
}
}

The above class makes asynchronous calls to WCF service to retrieve Data. The method ‘GetCompanyNames’ retrieves Company Names and adds them in the ‘ListCompanies’ List<T> collection. This is used to bind with UI.

The method ‘LoadSalesData’ accepts the CompanyId and gets the Sales details for the matching CompanyID. The received data is then added into the ‘ListSalesEndingQty’ List<T>. This object is further used for drawing the chart.

Task 8: In the project, add a new class file name it as ‘BusinessClassRepository.cs’. Add the following implementation:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;

namespace Store_CS_LineChart
{
/// <summary>
/// The structure which provides the Date and the EndSale
/// </summary>
public struct EndingSale
{
  DateTime _Date;
  public DateTime Date
  {
   get { return _Date; }
   set { _Date = value; }
  }
  int _EndSale;
  public int EndSale
  {
   get { return _EndSale; }
   set { _EndSale = value; }
  }
  public override string ToString()
  {
   return Date.ToString() + ": " + EndSale;
  }
}

/// <summary>
/// Class which will be
/// </summary>
public class SalesEndingQuantity
{
  int _CompanyID;
  public int CompanyID
  {
   get { return _CompanyID; }
   set { _CompanyID = value; }
  }
  List<EndingSale> _SaleQty = new List<EndingSale>();
  public List<EndingSale> SaleQty
  {
   get { return _SaleQty; }
   set { _SaleQty = value; }
  }
}

/// <summary>
/// Class for providing Data to UI for drawing the Graph
/// </summary>
public class DataProviderClass
{
  ObservableCollection<SalesEndingQuantity> _ListSalesDetails;
  public ObservableCollection<SalesEndingQuantity> ListSalesDetails
  {
   get { return _ListSalesDetails; }
   set { _ListSalesDetails = value; }
  }
  ModelClass objDs;
  public DataProviderClass()
  {
   ListSalesDetails = new ObservableCollection<SalesEndingQuantity>();
   objDs = new ModelClass();
  }
  /// <summary>
  /// Method to provide the Sales Quantity based upon the
  /// </summary>
  /// <param name="id"></param>
  /// <returns></returns>
  public async  Task GetSalesQuantityByCompanyId(int id)
  {
   await objDs.LoadSalesData(id);
   var FilteredData = objDs.ListSalesEndingQty.OrderBy(s=>s.EndSale);
   //Now put the data into the "ListSalesDetails" object.
   //This will be used to generate graph
   SalesEndingQuantity currentSales = new SalesEndingQuantity ();
   foreach (var item in FilteredData)
   {
    EndingSale endSale = new EndingSale();
    endSale.Date = item.SaleDate;
    endSale.EndSale = item.EndSale;
    currentSales.SaleQty.Add(endSale);
    ListSalesDetails.Add(currentSales);
   }
  }
}
}

The structure ‘EndingSale’ defines Date and EndSale properties. These properties will be used for the further calculation to find out the Min and Max Sale and also for Start and End Date of the Sale. The class ‘SalesEndingQuantiy’ is used to provide CompanyID and the List of the Sales for the companies.

The ‘DataProvider’ class contains the ‘GetSalesQuantityByCompanyId’ async method. This method calls the ModelClass define in the Task 7. This method is used to arrange the sales details in ascending order of the sale quantity. This is required to draw the Scale for the Chart. This method puts the data in ‘ListSalesDetails’ object of the type List<T>.

Task 9: The MainPage.xaml defines the UI layout that looks as follows

xaml-layout

The XAML below gives us an idea of the controls in place. Some of the formatting options have been removed for readability so kindly do not copy paste from here. Use the code view from the GitHub repo instead.

<Grid>
… <!-- Layout removed for brevity-->
<Grid Grid.Column="2" Grid.Row="1" Grid.RowSpan="2" Margin="20">
  <Grid.RowDefinitions>
   … <!-- Layout removed for brevity-->
  </Grid.RowDefinitions>
  <TextBlock FontFamily="Segoe UI" FontSize="20">List of Companies:</TextBlock>
  <!--List for showing List of Companies-->
  <ListBox x:Name="companyList" Width="200"
   Grid.Row="1" VerticalAlignment="Top" ItemsSource="{Binding Companies}"
   DisplayMemberPath="CompanyName"
   SelectedValuePath="CompanyID"
   SelectionChanged="companyList_SelectionChanged">
  </ListBox>
  <!--Radio button which user will select for type of Chart-->
  <RadioButton x:Name="rdbtnlinechart" Content="Line Graph"  Grid.Row="1" Margin="0,176,0,433"  Click="rdbtnlinechart_Click"/>
  <RadioButton x:Name="rdbtnpointchart" Content="Point Graph" Grid.Row="1" Margin="0,204,0,405" Click="rdbtnpointchart_Click"/>
</Grid>
<!--ViewBox which will act as a Platform for showing Chart-->
<Viewbox Grid.Column="1" Grid.Row="1" Stretch="UniformToFill"
Height="300" Width="300">
<Grid>
  <Canvas x:Name="linegraphCanvas" Width="100" Height="100" Background="Azure">
   <Line X1="0" Y1="99.75" X2="100" Y2="99.75" Stroke="Yellow" StrokeThickness="0.5" />
   <Line X1="0" Y1="0" X2="0" Y2="100" Stroke="Yellow" StrokeThickness="0.5" />
  </Canvas>
</Grid>
</Viewbox>

<!-- Some formatting has been removed for brevity-->
<TextBlock x:Name="txtminsale" Grid.Column="1" Margin="316,507,0,0" Grid.Row="1"/>

<TextBlock x:Name="txtmaxsale" Grid.ColumnSpan="2" HorizontalAlignment="Left" Height="25" Margin="316,196,0,0" Grid.Row="1" />

<TextBlock x:Name="txtstartdate" Grid.Column="1" HorizontalAlignment="Left" Margin="413,542,0,0" Grid.Row="1" />

<TextBlock x:Name="txtenddate" Grid.Column="1" HorizontalAlignment="Left" Margin="621,538,0,0" Grid.Row="1" />

<TextBlock Grid.ColumnSpan="2" HorizontalAlignment="Left" Height="27" Margin="521,575,0,0" Grid.RowSpan="2" />

<TextBlock Grid.ColumnSpan="2" HorizontalAlignment="Left" Height="25" Margin="296.158,370.01,0,0" Grid.RowSpan="2" TextWrapping="Wrap" Text="Sales" VerticalAlignment="Top" Width="48.98" RenderTransformOrigin="1.326,0.597" FontSize="20" FontFamily="Segoe UI" UseLayoutRounding="False" d:LayoutRounding="Auto">
  <TextBlock.RenderTransform>
   <CompositeTransform Rotation="-90.163" TranslateX="-38.149" TranslateY="-42.894"/>
  </TextBlock.RenderTransform>
</TextBlock>
</Grid>

(Note: There is a Binding expression set for the ListBox for Company Names. We will be performing binding in the next task).

Task 10: Open the code behind for the MainPage.xaml and use the following references:

using System;
using System.Linq;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
using Windows.UI.Xaml.Shapes;
using Windows.UI;
using System.Collections.ObjectModel;
using Store_CS_LineChart.MyRef;

Define the following properties at the class level:

ObservableCollection<CompanyMaster> _Companies;
public ObservableCollection<CompanyMaster> Companies
{
get { return _Companies; }
set { _Companies = value; }
}
ObservableCollection<SalesEndingQuantity> _Sales;
public ObservableCollection<SalesEndingQuantity> Sales
{
get { return _Sales; }
set { _Sales = value; }
}
ModelClass obj;
DataProviderClass objDs;
string chartType = string.Empty;

Add the following method in the code behind class:

/// <summary>
/// Method to generate the Grid Lines
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="rows"></param>
/// <param name="columns"></param>
public void GenerateGridLines(double width, double height, int rows, int columns)
{
GeometryGroup glines = new GeometryGroup();
Windows.UI.Xaml.Shapes.Path p = new Windows.UI.Xaml.Shapes.Path();
for (int currentRow = 1; currentRow < rows; ++currentRow)
{
  double pos = (height * currentRow) / ((double)rows);
  LineGeometry line = new LineGeometry();
  line.StartPoint = new Point(0, pos);
  line.EndPoint = new Point(width, pos);
  glines.Children.Add(line);
}
for (int currentColumn = 1; currentColumn < columns; ++currentColumn)
{
  double pos = (width * currentColumn) / ((double)columns);
  LineGeometry line = new LineGeometry();
  line.StartPoint = new Point(pos, 0);
  line.EndPoint = new Point(pos, height);
  glines.Children.Add(line);
}
p.Stroke = new SolidColorBrush(Colors.Blue);
p.Data = glines;
linegraphCanvas.Children.Add(p);
}

The above method is used to draw Rows and columns in the canvas for Chart. The above code makes used of the ‘GeometryGroup’ class, this class represents the composite geometry which is composed using the other geometry objects. The ‘LineGeometry’ class represents geometry for lines with Start and End point which is used to draw line.

Add the following code in the Loaded event:

/// <summary>
/// The Loaded event which will fill data in the ListBox.
/// This will also show the GridLines for Graph.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
async void MainPage_Loaded(object sender, RoutedEventArgs e)
{
obj = new ModelClass();
Companies = new ObservableCollection<CompanyMaster>();
await obj.GetCompanyNames();
Companies = obj.ListCompanies;
 
this.DataContext = this;
GenerateGridLines(linegraphCanvas.Width, linegraphCanvas.Height, 5, 5);
}

The above code makes call to the ModelClass and receive CompanyNames in the Companies property. The DataContext will bind the public property from the Code-behind with UI.

Run the application and the UI will looks similar to the one shown below

result-initial

Add Click event of the Radio Button, this will be used to decide upon the type of chart to be generated.

private void rdbtnlinechart_Click(object sender, RoutedEventArgs e)
{
if (rdbtnlinechart.IsChecked==true)
{
  chartType = "Line";
}
}
private void rdbtnpointchart_Click(object sender, RoutedEventArgs e)
{
if (rdbtnpointchart.IsChecked == true)
{
  chartType = "Point";
}
}

Add the following methods whichwill be used to Draw Line and Point Charts based upon the sale values received from the DataProvider class:

/// <summary>
/// Method to Draw Line Graph
/// </summary>
/// <param name="points"></param>
/// <param name="min"></param>
/// <param name="max"></param>
void DrawLineChart(Point[] points,int min,int max)
{
linegraphCanvas.Children.Clear(); //Clear All Children
//Draw the Grid Line
GenerateGridLines(linegraphCanvas.Width, linegraphCanvas.Height, 5, 5);
int Range = max - min;
//The Scale for Chart
double Scale = (Range == 0) ? 1.0 : 100.0 / ((double)Range);
PointCollection pointData = new PointCollection();
//Get the X,Y Point collection based upon value of the Sale. Here the EndSale value is selected
for (int i = 0; i < points.Length; i++)
{
  int sale = Sales[0].SaleQty[i].EndSale;
  int diff_Max_Min = max - sale;
  double yPoint = ((double)diff_Max_Min) * Scale;
  double xPoint = (i * 100) / ((double)(points.Length - 1));
  points[i] = new Point(xPoint, yPoint);
  pointData.Add(points[i]);
}
Polyline pline = new Polyline();
pline.StrokeThickness = 1;
pline.Stroke = new SolidColorBrush(Colors.Red);
pline.Points = pointData;
linegraphCanvas.Children.Add(pline);
}
/// <summary>
/// Method to Draw the Point Graph
/// </summary>
/// <param name="points"></param>
/// <param name="min"></param>
/// <param name="max"></param>
void DrawPointChart(Point[] points, int min, int max)
{
linegraphCanvas.Children.Clear();
//Draw the Grid Line
GenerateGridLines(linegraphCanvas.Width, linegraphCanvas.Height, 5, 5);
int Range = max - min;
//The Scale for Chart
double Scale = (Range == 0) ? 1.0 :  100.0 / ((double)Range);
PointCollection pointData = new PointCollection();
//EllipseGeometry for the PointChart
EllipseGeometry[] elpg = new EllipseGeometry[Sales[0].SaleQty.Count];
//Get the X,Y Point collection based upon value of the Sale. Here the EndSale value is selected
for (int i = 0; i < points.Length; i++)
{
  int sale = Sales[0].SaleQty[i].EndSale;
  int diff_Max_Min = max - sale;
  double yPoint = ((double)diff_Max_Min) * Scale;
  double xPoint = (i * 100) / ((double)(points.Length - 1));
  points[i] = new Point(xPoint, yPoint);
  elpg[i] = new EllipseGeometry();
  elpg[i].Center = points[i];
  elpg[i].RadiusX = 2;
  elpg[i].RadiusY = 2;
  Windows.UI.Xaml.Shapes.Path p = new Windows.UI.Xaml.Shapes.Path();
  p.Fill = new SolidColorBrush(Colors.Yellow);
  p.Stroke = new SolidColorBrush(Colors.Black);
  p.StrokeThickness = 0.5;
  p.Data = elpg[i];
  pointData.Add(points[i]);
  linegraphCanvas.Children.Add(p);
}
Polyline pline = new Polyline();
pline.StrokeThickness = 0.2;
pline.Stroke = new SolidColorBrush(Colors.Red);
pline.Points = pointData;
linegraphCanvas.Children.Add(pline);
}

The above two methods accept the Point array and min and max value of the Sale. The ‘DrawLineChart’ method has the following logic:

  • Clears the canvas so that the previous graph (if any) is removed.
  • Draw grid lines by making call to the ‘GenerateGridLines’ method.
  • Define the range and scale for drawing points.
  • The ‘for’ loop iterates through the Points array and creates X and Y Coordinate for Point collection.
  • The Polyline object accepts this point collection and then this Polyline is added as a child of the Canvas.

The ‘DrawPointChart’ method has the following logic:

  • Clears the canvas so that the previous graph (if any) is removed.
  • Draws grid lines by making call to the ‘GenerateGridLines’ method.
  • Defines the range and scale for drawing points.
  • The ‘for’ loop iterates through the Points array and create X and Y Coordinate for Point collection. This loop define an object of the ‘EllipseGrometry’. This class is used to draw point which represent the sale. This ‘EllipseGeometry’ is added as a Data to the ‘Path’ graphical element.
  • The Polyline object accepts this point collection and then this Polyline is added as a child of the Canvas.

Add the following code in the ‘SelectionChanged’ event of the CompanyList ListBox.

/// <summary>
/// The method which will get the CompanyId from the ListBox.
/// The call to the Model class
/// is made and the salesdetails for based upon the companyID is fetched.
/// From this received data, the Minimum and Maximum sales and the Start
/// and the End date for the sales is find out.
/// This method then draw Line and Point graph based upon the
/// calculations.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void companyList_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
//The selected Company Id
var companyId = companyList.SelectedValue;
//Get the Sales Data based upon the Company Id
objDs = new DataProviderClass();
await objDs.GetSalesQuantityByCompanyId(Convert.ToInt32(companyId));
Sales = objDs.ListSalesDetails;
//Start Logic for the Min and Max Sales and Start and End Dates of Sale
int minSale = Sales[0].SaleQty[0].EndSale; //The Sale at First (Zeroth index)
int maxSale = minSale;
foreach (EndingSale sale in Sales[0].SaleQty)
{
  minSale = Math.Min(minSale, sale.EndSale);
  maxSale = Math.Max(maxSale,sale.EndSale);
}
//Display the Minimum Sale value and Max Sale Value for the Chart
txtminsale.Text = minSale.ToString();
txtmaxsale.Text = maxSale.ToString();
//Sort the Sale Quantity by Date
var datewise = Sales[0].SaleQty.OrderBy(d => d.Date).ToList();
txtstartdate.Text = datewise[0].Date.ToString();
int lastRecord = datewise.Count - 1;
txtenddate.Text = datewise[lastRecord].Date.ToString();
//Ends Logic for the Min and Max Sales and Start and End Dates of Sale
Point[] graphPoints = new Point[Sales[0].SaleQty.Count];
//The Points collection used for the Chart
switch (chartType)
{
  case "Line":
   //Draw Line Chart
   DrawLineChart(graphPoints,minSale,maxSale);
   break;
  case "Point":
   //Draw Point Chart
   DrawPointChart(graphPoints, minSale, maxSale);
   break;
  default:
   var msg = new Windows.UI.Popups.MessageDialog("Please select the Chart Type");
   await msg.ShowAsync();
   break;
}
}

The above code does the following:

  • Gets the CompanyID based upon the end-users selection.
  • Based upon this CompanyID, makes call to the ‘GetSalesQuantityByCompanyId()’ method from the DataProvider class.
  • Once the data is received, the min and max sale value is found out and displayed on the UI.
  • The Start and End date of Sale is found out from the Sales quantity and displayed on UI.
  • The Switch-Case is used based upon which whether LineChart or Point Chart is drawn.

Task 11: Run the application, select the Graph Type (Line or Point) and select the Company Name, the chart will be drawn as below:

line-chart

point-chart

Conclusion

The 2D graphics API in WinRT can be used easily using C# and XAML to develop charts as per end-users requirements for Business Applications on Window 8 devices.

Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+
Further Reading - Articles You May Like!
Author
Mahesh Sabnis is a DotNetCurry author and Microsoft MVP having over 17 years of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions). Follow him on twitter @maheshdotnet


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!
Comment posted by Hyder on Friday, August 30, 2013 2:32 AM
Nice

Categories

JOIN OUR COMMUNITY

POPULAR ARTICLES

FREE .NET MAGAZINES

Free DNC .NET Magazine

Tags

JQUERY COOKBOOK

jQuery CookBook