WPF Book Control

The first time I was introduced to iBook, I was far from embracing this whole new reading concept.

I have yet the vivid memory of my parent quoting Spanish philosopher Marcelino Mendez y Pelayo – “Que lastima tenerme que morir, cuando me queda tanto por leer” It translates “It Is a shame I have to die, with so many books yet to read

I cannot avoid wondering whether Mendez y Pelayo would have welcome iBooks, and sacrifice the touch of the paper fiber at the tips of his hands with ease.

Despite early hesitations of the community to give up the millenary art of flipping a page, Book-Like interfaces have been able to engage audiences in a very profound way.

From Enterprise Report to Book-Like Interfaces

Recent book controls support the creation of Book like reporting mechanism, if you get a hold of a license of such a control then more power to you. In this case, you would not find the need to create a path way to render your content on a iBook like control.

On the other hand, for instance, your Company may be using a series of reports at its enterprise level, each application introducing a report through its report viewer. In order to implement a Book-Like experience for your users, you must create a path way from the already created report, to its rendering in an Book-Like control.

A common Path way

Export Book Pathway

Decision Making

Book-Like interfaces, today, come in various shapes and forms. We could find server side options such as the ones for Windows Presentation Foundation (WPF) and Silverlight introduced by ComponentOne and Telerik. There is one even introduced at CodePlex

If you are seeking to present in various platforms i.e. iOS and Android tablets as well as their mobile equivalency. It is encourage you to choose among the wide array of JavaScript solutions – here.

I would also encourage to place a good attention to TurnJS - a Javascript library that not only provides a rich API, but that it is also cross browser compatible.

The Book Like Interface Example

Generating the Report Object

After Introducing the stock market example, We will use the same source in order to draw a comparison between two market securities. In order to generate a more appropriate content, I chose to scrape the ticket symbols of the first twenty five S&P 500 Securities from Wikipidia.

In order to review our last post, this is how we produced a chart object in Telerik Report

   1: public static Telerik.Reporting.Chart CompareTwoStocks(string _securityOne, string _securityTwo)
   2:         {
   3:             Telerik.Reporting.Chart mychart = new Telerik.Reporting.Chart();
   4:             mychart.Skin = "Office2007";
   5:             string[] Stocks = new string[] { _securityOne, _securityTwo };
   6:             System.Drawing.Color[] seriesColor = new Color[] { Color.Red, Color.Blue };
   7:             List<HistoricalStock>[] stockitems = new List<HistoricalStock>[2];
   8:  
   9:             int size = 0;
  10:             foreach (string symbol in Stocks)
  11:             {
  12:                 stockitems[size++] = new List<HistoricalStock>(YahooHistoricalLoader.DownloadData(symbol, 2012).Where(f => f.Date >= new DateTime(2012, 1, 1)).OrderBy(t => t.Date).Select(m => m));
  13:             }
  14:  
  15:             #region XAxis and YAxis Labels
  16:             mychart.PlotArea.XAxis.AxisLabel.TextBlock.Visible = true;
  17:             mychart.PlotArea.YAxis.AxisLabel.TextBlock.Visible = true;
  18:             mychart.PlotArea.XAxis.AxisLabel.Visible = true;
  19:             mychart.PlotArea.YAxis.AxisLabel.Visible = true;
  20:             mychart.PlotArea.XAxis.AxisLabel.TextBlock.Text = "Period";
  21:             mychart.PlotArea.YAxis.AxisLabel.TextBlock.Text = "Price Per Share ($)";
  22:             mychart.PlotArea.XAxis.AxisLabel.Appearance.Position.Auto = true;
  23:             mychart.PlotArea.YAxis.AxisLabel.Appearance.Position.Auto = true;
  24:             #endregion
  25:  
  26:             mychart.ChartTitle.TextBlock.Text = string.Join(" Vs. ", Stocks);
  27:             mychart.ChartTitle.TextBlock.Appearance.TextProperties.Color = Color.Blue;
  28:             #region Generate Chart Series
  29:             for (int i = 0; i < size; i++)
  30:             {
  31:                 ChartSeries series = new ChartSeries();
  32:                 int seriesItemPosition = 0;
  33:                 foreach (HistoricalStock hs in stockitems[i])
  34:                 {
  35:                     ChartSeriesItem seriesItem = new ChartSeriesItem();
  36:                     seriesItem.YValue = hs.Close;
  37:                     seriesItem.XValue = seriesItemPosition++;
  38:                     seriesItem.Label.TextBlock.Visible = false;
  39:                     series.Items.Add(seriesItem);
  40:                 }
  41:                 series.YAxisType = (i == 0) ? ChartYAxisType.Primary : ChartYAxisType.Secondary;
  42:                 series.Appearance.FillStyle.MainColor = seriesColor[i];
  43:                 series.Name = Stocks[i];
  44:                 series.Appearance.LegendDisplayMode = ChartSeriesLegendDisplayMode.Nothing;
  45:                 series.Type = ChartSeriesType.Spline;
  46:                 mychart.Series.Add(series);
  47:  
  48:             }
  49:             #endregion
  50:  
  51:             #region Custom Legend
  52:             int seq = 0;
  53:             foreach (string symbol in Stocks)
  54:             {
  55:                 LabelItem LegenItem = new LabelItem();
  56:                 LegenItem.Marker.Visible = true;
  57:                 LegenItem.Marker.Appearance.Figure = Telerik.Reporting.Charting.Styles.DefaultFigures.Rectangle;
  58:                 LegenItem.Marker.Appearance.FillStyle.MainColor = seriesColor[seq++];
  59:                 LegenItem.Marker.Appearance.FillStyle.FillType = Telerik.Reporting.Charting.Styles.FillType.Solid;
  60:                 LegenItem.TextBlock.Text = symbol;
  61:  
  62:                 mychart.Legend.Items.Add(LegenItem);
  63:             }
  64:             #region Legend Location
  65:             mychart.Legend.Appearance.Location = Telerik.Reporting.Charting.Styles.LabelLocation.OutsidePlotArea;
  66:             mychart.Legend.Appearance.Overflow = Telerik.Reporting.Charting.Styles.Overflow.Row;
  67:             mychart.Legend.Appearance.Position.AlignedPosition = Telerik.Reporting.Charting.Styles.AlignedPositions.Bottom;
  68:             #endregion
  69:             #endregion
  70:             #region EliminateGridlines
  71:             mychart.PlotArea.YAxis.Appearance.MajorGridLines.Visible = false;
  72:             mychart.PlotArea.YAxis.Appearance.MinorGridLines.Visible = false;
  73:             mychart.PlotArea.XAxis.Appearance.MajorGridLines.Visible = false;
  74:             mychart.PlotArea.XAxis.Appearance.MinorGridLines.Visible = false;
  75:             #endregion
  76:  
  77:             #region Conguring XAxis
  78:             mychart.PlotArea.XAxis.Appearance.MinorTick.Visible = false;
  79:             mychart.PlotArea.XAxis.Appearance.MajorTick.Visible = false;
  80:             mychart.PlotArea.XAxis.Appearance.TextAppearance.TextProperties.Font = new System.Drawing.Font(FontFamily.GenericSansSerif, 6, FontStyle.Regular);
  81:             mychart.PlotArea.XAxis.LayoutMode = Telerik.Reporting.Charting.Styles.ChartAxisLayoutMode.Between;
  82:             mychart.PlotArea.XAxis.IsZeroBased = true;
  83:             mychart.PlotArea.XAxis.AutoScale = false;
  84:             mychart.PlotArea.XAxis.AddRange(1, stockitems[0].Count(), 1);
  85:             int sequence = 0;
  86:             foreach (HistoricalStock h in stockitems[0])
  87:             {
  88:  
  89:                 mychart.PlotArea.XAxis[sequence].TextBlock.Text = h.Date.ToString("MMM, yyyy");
  90:                 mychart.PlotArea.XAxis[sequence].TextBlock.Visible = false;
  91:                 sequence++;
  92:  
  93:             }
  94:  
  95:             #endregion
  96:  
  97:             #region Scaling Chart YAxis
  98:            
  99:             mychart.PlotArea.YAxis.IsZeroBased = false;
 100:             mychart.PlotArea.YAxis.AutoScale = true;
 101:            
 102:             mychart.PlotArea.YAxis2.IsZeroBased = false;
 103:             mychart.PlotArea.YAxis2.AutoScale = true;
 104:           
 105:             #endregion
 106:  
 107:             #region Zonification
 108:             int[] ZoningSize = stockitems[0].GroupBy(p => p.Date.Month).Select(t => t.Count()).ToArray();
 109:             int counter = 0;
 110:             int monthSeq = 1;
 111:             foreach (int z in ZoningSize)
 112:             {
 113:                 ChartMarkedZone zone = new ChartMarkedZone();
 114:                 zone.ValueStartX = counter;
 115:                 zone.ValueEndX = counter + z;
 116:                 counter = counter + z;
 117:                 zone.Label.TextBlock.Text = new DateTime(2012, monthSeq, 1).ToString("MMM");
 118:                 zone.Appearance.Border.Color = Color.LightBlue;
 119:                 zone.Label.Appearance.Position.AlignedPosition = Telerik.Reporting.Charting.Styles.AlignedPositions.Bottom;
 120:                 zone.Label.TextBlock.Appearance.TextProperties.Font = new System.Drawing.Font(System.Drawing.FontFamily.GenericSansSerif, 5, FontStyle.Bold);
 121:                 monthSeq++;
 122:                 mychart.PlotArea.MarkedZones.Add(zone);
 123:             }
 124:             #endregion
 125:  
 126:             #region Chart Location and Customization
 127:             Telerik.Reporting.Drawing.Unit Height = new Telerik.Reporting.Drawing.Unit(7, ((Telerik.Reporting.Drawing.UnitType)(Telerik.Reporting.Drawing.UnitType.Cm)));
 128:             Telerik.Reporting.Drawing.Unit Width = new Telerik.Reporting.Drawing.Unit(14, ((Telerik.Reporting.Drawing.UnitType)(Telerik.Reporting.Drawing.UnitType.Cm)));
 129:             mychart.PlotArea.XAxis.AutoShrink = false;
 130:             mychart.Size = new Telerik.Reporting.Drawing.SizeU(Width, Height);
 131:             mychart.Location = new Telerik.Reporting.Drawing.PointU(new Telerik.Reporting.Drawing.Unit(0.25, ((Telerik.Reporting.Drawing.UnitType)(Telerik.Reporting.Drawing.UnitType.Cm))), new Telerik.Reporting.Drawing.Unit(0.8, ((Telerik.Reporting.Drawing.UnitType)(Telerik.Reporting.Drawing.UnitType.Cm))));
 132:  
 133:             #region Reposition XAxis Label
 134:             mychart.PlotArea.XAxis.AxisLabel.Appearance.Position.Auto = false;
 135:             mychart.PlotArea.XAxis.AxisLabel.Appearance.Position.X = 320;
 136:             mychart.PlotArea.XAxis.AxisLabel.Appearance.Position.Y = 320;
 137:             #endregion
 138:             #region Reposition Graph
 139:             mychart.PlotArea.Appearance.Dimensions.Margins.Bottom = 90;
 140:             mychart.PlotArea.Appearance.Dimensions.Margins.Right = 80;
 141:             mychart.PlotArea.Appearance.Dimensions.Margins.Left = 90;
 142:             #endregion
 143:             #endregion
 144:  
 145:             return mychart;
 146:         }

 

In this example, We are going to display more than one report object – more than one chart; as a result, we will pass more than one chart to the report object. We would utilized the HtmlAgilityPack to scrape the name of the S&P 500 Securities from Wikipedia. Subsequently, we would proceed to select two securities for every report;

   1: public static List<Telerik.Reporting.Chart> GetListofCharts()
   2:         {
   3:             List<Telerik.Reporting.Chart> myStockCharts = new List<Telerik.Reporting.Chart>();
   4:  
   5:             string url = "http://en.wikipedia.org/wiki/List_of_S%26P_500_companies";
   6:             HtmlDocument doc = new HtmlWeb().Load(url);
   7:  
   8:             var securities = (from n in doc.DocumentNode.SelectNodes("//a") where n.OuterHtml.IndexOf("class=\"external text\"") != -1 && n.OuterHtml.IndexOf("quickquote") != -1 select n).ToList();
   9:            
  10:             int i = 0;
  11:             var query = from s in securities
  12:                         let seq = i++
  13:                         group s by seq / 2 into gr
  14:                         select gr.ToArray();
  15:  
  16:             int counter = 0;
  17:             foreach (var t in query)
  18:             {
  19:                 if (counter < 50)
  20:                 myStockCharts.Add( CompareTwoStocks(t[0].InnerText, t[1].InnerText));
  21:  
  22:                 counter++;
  23:             }
  24:  
  25:             return myStockCharts;
  26:         }

In Telerik Report, we add the chart objects through group object added to the report

   1: public void GenerateReporCharts()
   2:        {
   3:            foreach (Telerik.Reporting.Chart chart in TableCreator.GetListofCharts())
   4:            {
   5:                var Group1 = new Group(true);
   6:                Group1.GroupFooter.Visible = false;
   7:                Group1.GroupHeader.Visible = true;
   8:                Group1.GroupHeader.PageBreak = PageBreak.None; //PageBreak.After;
   9:                Group1.GroupHeader.Style.VerticalAlign = VerticalAlign.Middle;
  10:                Group1.GroupHeader.KeepTogether = true;
  11:                System.Guid GroupGuid = System.Guid.NewGuid();
  12:                Group1.Name = GroupGuid.ToString();
  13:                Group1.GroupHeader.Items.Add((ReportItemBase)(chart));
  14:                this.Groups.Add(Group1);
  15:                Group1.Dispose();
  16:            }
  17:        }

Below is the resulting report, generated in the design report viewer.

 

ReportResult

Generating Resource for the Book Interface

After successfully generating our report, we must create the resources that will enable the rendering of the book control, or the book interface object. Existing book controls provide the opportunity of rendering PDF file formats into the Book object. If your book control presents this opportunity, the only pending action would be to export the report content into a PDF file. In our case, however, I would export resources into images. Most book controls enable the display of images in their Page object.

It would take a little understanding of the report API in question in order to perform the export dynamically. In the case of Telerik Reporting, the render RenderReport Class converts the contents of the reports to bytes, thus you could use this class to export the report in the specified format. Here is some more information on the available formats to export.

   1: private static string _DirectoryName = "BookItems";
   2: private static string _fullpath;
   3: private static List<Stream> _streams = new List<Stream>();
   4: private static int _sequence = 0;
   5: private static int _imagesequence = 0;
   6:  
   7:  
   8: public static void ExportToHTML(Telerik.Reporting.Report reportToExport)
   9: {
  10:  
  11:     _fullpath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\" + _DirectoryName;
  12:     CreateDirectory(); // Clean the Directory
  13:     ReportProcessor reportProcessor = new ReportProcessor();
  14:     var documentName = "";
  15:     var deviceInfo = new System.Collections.Hashtable();
  16:     reportProcessor.RenderReport("HTML", reportToExport, deviceInfo, CreateStream, out documentName);
  17:     CloseStreams();
  18:     CreateReportImages();
  19: }

 

Remember also that it is key that we place the exported resources in a known folder for later use. Therefore, “BookItems” will be our special folder to use toward this end. We clean the directory in case it exits, and created in case it is not there.

   1: private static void CreateDirectory()
   2: {
   3:     
   4:     DirectoryInfo dir = new DirectoryInfo(_fullpath);
   5:     if (!dir.Exists)
   6:     {
   7:         dir.Create();
   8:     }
   9:     else
  10:     {
  11:         //clean the direcotry
  12:         foreach (FileInfo drin in dir.GetFiles())
  13:         {
  14:             drin.Delete();
  15:  
  16:         }
  17:     }
  18:  
  19: }

The next step is to create the stream object, for every HTML exported we would provide it with a sequential number, only in this way, we would know its proper order in the list.

   1: public static Stream CreateStream(string name, string extension, Encoding encoding, string mimeType)
   2: {
   3:     string reportsPath = _fullpath;
   4:     string imagesPath = _fullpath;
   5:     var path = reportsPath;
   6:     bool IsImage = false;
   7:     if (mimeType.ToLowerInvariant().Contains("image"))
   8:     {
   9:         path = imagesPath;
  10:         IsImage = true;
  11:     }
  12:     /// if not an Image give it an ID
  13:     var filePath = (IsImage) ? Path.Combine(path, name + "." + extension) : Path.Combine(path, (_sequence) + "-" + name + "." + extension);  
  14:     var fs = new FileStream(filePath, FileMode.Create);
  15:     _sequence++;
  16:  
  17:     _streams.Add(fs);
  18:     return fs;
  19: }

Close the Stream in order to avoid the error of “File used by another process”

   1: private static void CloseStreams()
   2: {
   3:     foreach (var stream in _streams)
   4:     {
   5:         stream.Close();
   6:         stream.Dispose();
   7:     }
   8:     _streams.Clear();
   9: }

The above methods will export the report resource effectively to the “BookItems folder”

BookResources

So We have that the webpages exported successfully,  However, Our report Control may not be able to render HTML content. If this is the case, then we must proceed to convert the webpages into image files.

Here is simply one way to accomplished it

   1: public static void CreateReportImages()
   2:         {
   3:           
   4:             DirectoryInfo dir = new DirectoryInfo(_fullpath);
   5:             if (dir.GetFiles().Length > 0)
   6:             {
   7:                 string me;
   8:                 
   9:                 foreach (FileInfo fi in dir.GetFiles().Where(t => t.FullName.ToString().IndexOf("Page_") != -1).OrderBy(t => Convert.ToInt16(me = t.FullName.ToString().Substring(_fullpath.Length + 1, t.FullName.ToString().IndexOf("-") - (_fullpath.Length + 1)))).Select(m => m))
  10:                 {
  11:                     
  12:                         lock (fi)
  13:                         {
  14:  
  15:                             Thread thread = new Thread(new ThreadStart(new MethodInvoker(() => CreatePageImage(fi.FullName.ToString()))));
  16:                             thread.SetApartmentState(ApartmentState.STA);
  17:                             thread.Start();
  18:                             thread.Join();
  19:                             while (thread.IsAlive) System.Windows.Forms.Application.DoEvents();
  20:  
  21:                             thread = null;
  22:                         }
  23:  
  24:                     
  25:                 }
  26:             }
  27:  
  28:         }

 

Please note that we index each file by the number character that follows “Page_” This is so we could create the image in the same sequential order.

   1: public static void CreatePageImage(string _path)
   2:        {
   3:            
   4:            Bitmap bitmap = new Bitmap(CaptureWebPage(_path));
   5:            string filename = "NI-" + _imagesequence + ".jpeg";
   6:            bitmap.Save(_fullpath + @"\" + filename, System.Drawing.Imaging.ImageFormat.Jpeg);
   7:            bitmap.Dispose();
   8:            _imagesequence++;
   9:        }
  10:  
  11: public static System.Drawing.Bitmap CaptureWebPage(string URL)
  12:         {
  13:             // create a hidden web browser, which will navigate to the page
  14:             System.Windows.Forms.WebBrowser web = new System.Windows.Forms.WebBrowser();
  15:             // we don't want scrollbars on our image
  16:             web.ScrollBarsEnabled = false;
  17:             // don't let any errors shine through
  18:             web.ScriptErrorsSuppressed = true;
  19:             // let's load up that page!
  20:             web.Navigate(URL);
  21:  
  22:             // wait until the page is fully loaded
  23:             while (web.ReadyState != WebBrowserReadyState.Complete)
  24:                 System.Windows.Forms.Application.DoEvents();
  25:             System.Threading.Thread.Sleep(500); // allow time for page scripts to update,  it was 1500
  26:             // the appearance of the page
  27:  
  28:             // set the size of our web browser to be the same size as the page
  29:             int width = web.Document.Body.ScrollRectangle.Width;
  30:             int height = web.Document.Body.ScrollRectangle.Height;
  31:             web.Width = width;
  32:             web.Height = height;
  33:             // a bitmap that we will draw to
  34:             System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(width, height);
  35:             // draw the web browser to the bitmap
  36:             web.DrawToBitmap(bmp, new System.Drawing.Rectangle(0, 0, width, height));
  37:  
  38:             return bmp; // return the bitmap for processing
  39:         }

Displaying the Book Interface

We will use the WPF Telerik Book control in order to render a book with the resources we just exported.

   1: <Window x:Class="WpfAndReporting.iBook"
   2:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:           xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
   5:         xmlns:viewModels="clr-namespace:WpfAndReporting"
   6:         Title="iBook" Height="300" Width="300">
   7:     <Window.Resources>
   8:         <viewModels:PageCollection x:Key="Pages" />
   9:         <DataTemplate x:Key="PageTemplate">
  10:             <Border BorderBrush="#B2ADBDD1" BorderThickness="1">
  11:                 <Grid Background="White" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
  12:                     <Image Source="{Binding ImageSource}" />
  13:                 </Grid>
  14:             </Border>
  15:         </DataTemplate>
  16:     </Window.Resources>
  17:  
  18:         <DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" LastChildFill="True" Width="Auto" >
  19:             <Border Background="LightBlue" DockPanel.Dock="Top">
  20:                 <Border>
  21:                     <telerik:Label TextBlock.FontWeight="Bold" TextBlock.Foreground="White" DockPanel.Dock="Top">Report Like Book</telerik:Label>
  22:                 </Border>
  23:             </Border>
  24:      
  25:         <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
  26:             <Border CornerRadius="5">
  27:                 <telerik:RadBook x:Name="RadBook1" IsKeyboardNavigationEnabled="True"
  28:                     ItemsSource="{StaticResource Pages}"
  29:                     ItemTemplate="{StaticResource PageTemplate}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
  30:                     FirstPagePosition="Left" RightPageIndex="0" />
  31:             </Border>
  32:         </Grid>
  33:     </DockPanel>
  34: </Window>

 

And following, Our View Model:

   1: public class Page
   2:     {
   3:         public Page(string uri)
   4:         {
   5:             this.ImageSource = new Uri(uri, UriKind.RelativeOrAbsolute);
   6:         }
   7:  
   8:         public Uri ImageSource { get; set; }
   9:     }
  10:  
  11:     public class PageCollection : ObservableCollection<Page>
  12:     {
  13:         public PageCollection()
  14:         {
  15:  
  16:             Reporting.ProductReport pr = new Reporting.ProductReport();
  17:             BookViewCreator.ExportToHTML(pr);
  18:             string DirectoryName = "BookItems";
  19:             string FullPath;
  20:             string Directory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
  21:             Directory += @"\" + DirectoryName;
  22:             FullPath = Directory;
  23:             DirectoryInfo dir = new DirectoryInfo(Directory);
  24:             if (dir.GetFiles().Length > 0)
  25:             {
  26:                 int counter = 0;
  27:                 foreach (FileInfo fi in dir.GetFiles().Where(n => n.FullName.IndexOf("NI-") != -1).OrderBy(p => Convert.ToInt16(p.FullName.ToString().Substring(p.FullName.ToString().IndexOf("NI-") + "NI-".Length, p.FullName.ToString().Length - (p.FullName.ToString().IndexOf("NI-") + "NI-".Length + ".jpeg".Length)))).Select(t => t))
  28:                 {
  29:                             var urix = new Uri(fi.FullName.ToString());
  30:                             var convert = urix.AbsoluteUri;
  31:                             this.Add(new Page(convert));
  32:                             counter++;
  33:                       
  34:                 }
  35:             }
  36:  
  37:         }
  38:     }

My Enterprise Book Report

Demo7