How to create Apple iTunes and Google Play compatible RSS feed using ASP.NET

In order to create a podcast on Google Play or Apple iTunes, you need to create a valid RSS feed that conforms to the RSS 2.0 specifications and also to include their required tags. Luckily, both companies accept each other’s standards. In other words, if you decide to implement Apple’s tags, you won’t need to change or modify your feed to support Google’s standards.

In this article I’m going to explain how to create a feed that is compatible with both companies. Let’s assume you are building a feed for weekly church sermons, here is what you need to do:

1- Define a RSS element that includes iTunes & Atom namespace:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
</rss>

2- Inside the rss element, create a channel element which contains the following sub-elements:

  • <title> Title of the Podcast’s
  • <link> The feed’s URL
  • <atom:link> Same as <link>
  • <language> The ISO language used in the podcast. See http://www.loc.gov/standards/iso639-2/php/code_list.php for the languages list.
  • <copyright> The organization copyrights
  • <itunes:author> Author of the podcast (up to 255 characters)
  • <itunes:summary> Description of the podcast (up to 4000 characters)
  • <description> Same as summary
  • <itunes:owner> After the feed is submitted to either companies, they will send a verification email to the email provided in the sub element. This element contains two sub elements
    • <itunes:name> The organization’s name
      (up to 255 characters)
    • <itunes:email> Email address to authenticate podcast ownership (up to 255 characters). Make sure you have access to that email
  • <itunes:image> Image shown on Google Play or iTunes to identify your podcast (URL can include up to 2048 characters). The image has to be at least 1400 x 1400 pixels and at maximum 3000 x 3000 pixels in JPEG or PNG format
  • <itunes:category> The podcast’s category. See https://help.apple.com/itc/podcasts_connect/#/itc9267a2f12 for a complete supported category and subcategory list.
  • <itunes:explicit> If your podcast has explicit content, you need to add the appropriate tag on your podcast’s RSS feed. See https://support.google.com/googleplay/podcasts/answer/6260345 to learn more about identifying explicit content.

The following is an example of a valid channel element

<channel>
  <title>Church Sermon</title>
  <link>http://www.church.com/Sermon</link>
  <atom:link>http://www.church.com/Sermon</atom:link>
  <language>en-us</language>
  <copyright>Church Copyright</copyright>
  <itunes:author>Author</itunes:author>
  <itunes:summary>Weekly Sermons</itunes:summary>
  <description>Church Weekly Sermons</description>
  <itunes:owner>
    <itunes:name>Church</itunes:name>
    <itunes:email>church@church.com</itunes:email>
  </itunes:owner>
  <itunes:image href="http://www.church.com/img/church.jpg" />
  <itunes:category text="Religion & Spirituality">
    <itunes:category text="Christianity" />
  </itunes:category>
  <itunes:explicit>no</itunes:explicit>
</channel>

3- Create a list of item elements (one for each episode) inside the channel element. Each item with the following sub-elements:

  • <title> The episode name
  • <itunes:author> The episode speaker (up to 255 characters)
  • <itunes:summary> The episode description (up to 4000 characters)
  • <enclosure> This element contains three attributes:
    • URL: The URL attribute points to your podcast content
    • length: The length attribute is the file size in bytes
    • type: The type attribute provides the correct category for the type of file you are using. The type values for the supported file formats are: audio/x-m4a, audio/mpeg, video/quicktime, video/mp4, video/x-m4v, and application/pdf.
  • <guid> Permanent, case-sensitive Globally Unique Identifier for a podcast episode. Each episode needs a GUID. GUIDs are compared to indicate which episodes are new. If the guid is not a link, set the isPermaLink attribute to false (As shown in the example)
  • <pubDate> The episode publication date shown to users
  • <itunes:explicit> Described above.

The following is an example of a valid item

<item>
  <title>episode 1</title>
  <itunes:author>Fr. James</itunes:author>
  <itunes:summary>This is the first episode</itunes:summary>
  <enclosure url="http://church.com/sermons/episode1.mp4" type="audio/x-mp3" length="6957974" />
  <guid isPermaLink="false">episode1.mp4</guid>
  <pubDate>Sat, 19 Jan 2019 00:00:00 +00:00</pubDate>
  <itunes:explicit>no</itunes:explicit>
</item>

If we put together what we have discussed we’ll end up with the following implementation

[HttpGet]
[HttpHead]
[AllowAnonymous]
public void RssFeed()
{
    XNamespace itunesNs = "http://www.itunes.com/dtds/podcast-1.0.dtd";
    XNamespace atomNs = "http://www.w3.org/2005/Atom";
    var feed = new XDocument(
        new XDeclaration("1.0", "utf-8", "yes"),
        new XElement("rss",
        new XAttribute("version", "2.0"),
        new XAttribute(XNamespace.Xmlns + "itunes", itunesNs.NamespaceName),
        new XAttribute(XNamespace.Xmlns + "content", "http://purl.org/rss/1.0/modules/content/"),
        new XAttribute(XNamespace.Xmlns + "atom", atomNs.NamespaceName),
            new XElement("channel",
                new XElement("title", "Church Sermon"),
                new XElement("link", new Uri(Url.Action(nameof(Index), "Sermon", null, Request.Scheme))),
                new XElement(atomNs + "link", new Uri(Url.Action(nameof(Index), "Sermon", null, Request.Scheme))),
                new XElement("language", "en-us"),
                new XElement("copyright", "Church Copyright"),
                new XElement(itunesNs + "author", "Author"),
                new XElement(itunesNs + "summary", "Weekly Sermons"),
                new XElement("description", "Church Weekly Sermons"),
                new XElement(itunesNs + "owner",
                    new XElement(itunesNs + "name", "Church"),
                    new XElement(itunesNs + "email", "church@church.com")),
                new XElement(itunesNs + "image", new XAttribute("href", new Uri(Request.Scheme + "://" + Request.Host.Value + "/img/church.jpg"))),
                new XElement(itunesNs + "category", new XAttribute("text", "Religion & Spirituality"),
                    new XElement(itunesNs + "category", new XAttribute("text", "Christianity"))),
                new XElement(itunesNs + "explicit", "no"),
                _Context.Sermons.Include(s => s.Author).AsNoTracking().ToList()
                .Select(s =>
                {
                    return new XElement("item",
                        new XElement("title", s.Title),
                        new XElement(itunesNs + "author", "Fr. " + s.Author.FirstName + " " + s.Author.LastName),
                        new XElement(itunesNs + "summary", s.Title),
                        new XElement("enclosure", new XAttribute("url", s.AudioUrl), new XAttribute("type", "audio/x-mp3"), new XAttribute("length", s.Size)),
                        new XElement("guid", s.AudioBlobId, new XAttribute("isPermaLink", "false")),
                        new XElement("pubDate", s.Date.ToString("ddd, dd MMM yyyy HH:mm:ss zzzz")),
                        new XElement(itunesNs + "explicit", "no"));
                }))));

    Response.ContentType = "application/rss+xml";
    using (XmlWriter writer = XmlWriter.Create(Response.Body))
    {
        feed.WriteTo(writer);
        writer.Close();
    }

    Response.Body.Flush();
}

The above controller action method implementation supports Head requests, which is a way of allowing Google Play or iTunes to get information about the episodes without downloading them.

Also, keep in your mind that this method will be called frequently according to your podcast popularity. Consequently, I’d recommend for an actual implementation to cache the request, or at least the database calls (querying the sermons in this case).

Log4net for .Net Core 2.0

Log4net is a an excellent library that allows developers to output log statements to a variety of output targets through what is called Appenders. However, it’s not compatible with .Net Core 2.0 yet, and all the online log4net extension libraries available today don’t provide a thorough solution to rectify all log4net appenders specifically the ADOAppender; which logs to a database.  In this article I am going to introduce a solution to this problem.

I have uploaded the solution to GitHub at this URL:  https://github.com/rizksobhi/Log4net.NetCore

1- AdoNetAppender

I have created an appender that is compatible with .Net Core that can be used to log to a database. The code is straight forward and I have imported most of it from the log4net library. https://github.com/rizksobhi/Log4net.NetCore/blob/master/Log4net.NetCore.Lib/Appenders/AdoNetAppender.cs

2- Configuration file replacements

I have substituted the configuration file with a static configuration class which provides several ways of creating a variety of appenders. This configuration class uses the AdoNetAppender which is described above.

public static IAppender CreateAdoNetAppender(string connectionString)
{
    AdoNetAppender appender = new AdoNetAppender()
    {
        Name = "AdoNetAppender",
        BufferSize = 1,
        ConnectionType = "System.Data.SqlClient.SqlConnection, System.Data, Version = 1.0.3300.0, Culture = neutral, PublicKeyToken = b77a5c561934e089",
        ConnectionString = connectionString,
        CommandText = "INSERT INTO Log ([Date],[Thread],[Level],[Logger],[Message],[Exception]) VALUES (@log_date, @thread, @log_level, @logger, @message, @exception)"
    };

    AddDateTimeParameterToAppender(appender, "@log_date");
    AddStringParameterToAppender(appender, "@thread", 255, "%thread");
    AddStringParameterToAppender(appender, "@log_level", 50, "%level");
    AddStringParameterToAppender(appender, "@logger", 255, "%logger");
    AddStringParameterToAppender(appender, "@message", 4000, "%message");
    AddErrorParameterToAppender(appender, "@exception", 2000);

    appender.ActivateOptions();
    return appender;
}

3- ASP.Net Core integration

The following extension method helps to register log4net in the logging factory

public static ILoggerFactory AddLog4Net(this ILoggerFactory factory, string connectionString, string logFilePath)
{
    factory.AddProvider(new Log4NetProvider(connectionString, logFilePath));
    return factory;
}

Now we can register our log4net provider through using the extension method at the Configure function in Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, Log4netDBContext context, ILoggerFactory loggerFactory)
{
    loggerFactory.AddLog4Net(Configuration["Logging:ConnectionString"], Configuration["Logging:LogFilePath"]);

    DBInitializer.Initialize(context);

    if (env.IsDevelopment())
    {
        app.UseBrowserLink();
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }
            
    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Here is the implementation of Log4NetProvider https://github.com/rizksobhi/Log4net.NetCore/blob/master/Log4net.NetCore.Lib/Log4NetProvider.cs

And finally using in the HomeController

public HomeController(ILogger logger)
{
    _Logger = logger;
}
public IActionResult Index()
{
    _Logger.LogDebug("Index has been requested");
    return View();
}

References

  1. https://github.com/apache/logging-log4net
  2. https://dotnetthoughts.net/how-to-use-log4net-with-aspnetcore-for-logging/

DynamicMethod vs Reflection

Introduction

DynamicMethod was introduced in .Net 2.0, and in this article I am going to explain how to use DynamicMethod class as an alternative to Reflection to achieve a better performance.

DynamicMethod is the most efficient way to generate and execute a small amount of code. For instance, you can leverage DynamicMethod class to replace System.Reflection by generating and executing methods at run time, without having to generate dynamic assemblies and dynamic types to contain the methods. The executable code created by the JIT compiler will be reclaimed when the DynamicMethod object is reclaimed. This approach is much faster and provides exceptional performance comparing to using Reflection.

The Problem

System.Reflection is too slow and will cost you performance degradation if you decide to use it heavily in your application. However, in some domains like building compilers or ORM databases, using reflection is inevitable.

The Solution

Luckily, DynamicMethod could substitute Reflection, while offering a significant performance increase. To prove that, I have created an application which instantiates new objects in a tight loop using Reflection, DynamicMethod and regular object instantiation using new keyword.

InstantiateObject

The above screenshot shows the results of running the code in a tight loop 1,000,000,00 times. As shown, the DynamicMethod is roughly 6 times faster than Reflection (You may get different results when you try it on your machine), while is almost close to using the new keyword.

public delegate object InstanceCreator();
public InstanceCreator CreateInstance() where T : class
{
    InstanceCreator result = null;
    var constructor = typeof(T).GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[0], null);
    if (constructor != null)
    {
        var dynamicMethod = new DynamicMethod("InstantiateObject", MethodAttributes.Static | MethodAttributes.Public, CallingConventions.Standard, typeof(object), null, typeof(T), true);
        var codeGenerator = dynamicMethod?.GetILGenerator();
        if (codeGenerator != null)
        {
            codeGenerator.Emit(OpCodes.Newobj, constructor);
            codeGenerator.Emit(OpCodes.Ret);
            result = dynamicMethod.CreateDelegate(typeof(InstanceCreator)) as InstanceCreator;
        }
    }

    return result;
}

The InstanceCreator delegate is as a return type of the CreateInstance method which creates and compile code dynamically. The delegate then points to this generated code which can be invoked later to perform the specified task.

The same idea could be applied to create setters and getters delegates to set/get private fields using DynamicMethod class as shown blow

public delegate object Getter(object source);
public Getter CreateGetter(FieldInfo fieldInfo) where T : class
{
    Getter result = null;
    var dynamicGet = new DynamicMethod("DynamicGet", typeof(object), new Type[] { typeof(object) }, typeof(T), true);
    var getGenerator = dynamicGet?.GetILGenerator();
    if (getGenerator != null)
    {
        getGenerator.Emit(OpCodes.Ldarg_0);
        getGenerator.Emit(OpCodes.Ldfld, fieldInfo);
        BoxIfNeeded(fieldInfo.FieldType, getGenerator);
        getGenerator.Emit(OpCodes.Ret);
        result = dynamicGet.CreateDelegate(typeof(Getter)) as Getter;
    }

    return result;
}

public delegate void Setter(object source, object value);
public Setter CreateSetter(FieldInfo fieldInfo) where T : class
{
    Setter result = null;

    var dynamicSet = new DynamicMethod("DynamicSet", typeof(void), new Type[] { typeof(object), typeof(object) }, typeof(T), true);
    var setGenerator = dynamicSet?.GetILGenerator();

    if (setGenerator != null)
    {
        setGenerator.Emit(OpCodes.Ldarg_0);
        setGenerator.Emit(OpCodes.Ldarg_1);
        UnboxIfNeeded(fieldInfo.FieldType, setGenerator);
        setGenerator.Emit(OpCodes.Stfld, fieldInfo);
        setGenerator.Emit(OpCodes.Ret);

        result = dynamicSet.CreateDelegate(typeof(Setter)) as Setter;
    }
    return result;
}

private void BoxIfNeeded(Type type, ILGenerator generator)
{
    if (type.IsValueType)
        generator.Emit(OpCodes.Box, type);
}

private void UnboxIfNeeded(Type type, ILGenerator generator)
{
    if (type.IsValueType)
        generator.Emit(OpCodes.Unbox_Any, type);
}

You could encapsulate all these methods into a DynamicMethodFactory class and use them as the following

DynamicMethodFactory dynamicMethodFactory = new DynamicMethodFactory();
InstanceCreator instanceCreator = dynamicMethodFactory.CreateInstance();
object result = instanceCreator();
DynamicMethodFactory dynamicMethodFactory = new DynamicMethodFactory();
FieldInfo fieldInfo = typeof(TestClass).GetField("property", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
Setter setter = dynamicMethodFactory.CreateSetter(fieldInfo);
setter(testClass, "test");
DynamicMethodFactory dynamicMethodFactory = new DynamicMethodFactory();
FieldInfo fieldInfo = typeof(TestClass).GetField("property", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
Getter getter = dynamicMethodFactory.CreateGetter(fieldInfo);
string value = (string)getter(testClass);

 

Conclusion

DynamicMethod class offers a significant improve to system performance by creating and compiling code at run time. This approach is much faster than using System.Reflection as proven by the results above.

Thank you for reading my article. I wish it helps.

How to integrate PayPal with .Net Core

In this article I am going to explain how you can integrate PayPal express checkout with Asp .Net Core using REST API to process payments.

1- Download the sample project

I have created an Asp.net core project using VS 2017 for you. You can download it from Github https://github.com/rizksobhi/PaypalExpressCheckout

2- Create a PayPal App

Go to PayPal developer , and under REST API apps click Create App

CreateApp

3- Update ClientID and Client Secret

Copy the ClientID and Secret from Sandbox and update appsettings.json in the PayPalExpressCheckout.Web application

AppSecret

appsettings

4- Update Payee and Payer information

Go to PayPalServices and update the Payee email and merchant_id

PayeeInfo

For testing purposes, PayPal provides testing accounts that you can create under your PayPal account. You can create test buyer and seller accounts from Sandbox environment for testing

To create test accounts, go to Sandbox -> Accounts and click on Create Account and follow the instructions

TestPaymentAccounts

6- Build and Run the solution. YOU ARE ALL SET.

One of the Hyper components is not running

error

You will probably face this issue if you try to install a virtual machine inside a virtual machine. In my case, I was trying to install Android emulators inside a VM. Eventually I was able to fix this issue after a whole day of researching.

To fix this issue:

  1. Your VM must have at least 2 processors. Go to your VM settings -> Hardware -> Processors and change the Number of processors (Hint – This setting is changeable only when your VM is turned off)
  2. Open the directory where your VM is located, and locate the .vmx file. Right click and open with text editor.
  3. Add the following lines:
    1. hypervisor.cpuid.v0 = “FALSE”
    2. mce.enable = “TRUE”
    3. vhv.enable = “TRUE”
  4. Save and run your VM.

How to use Unity Interception to create Attribute Based Cache

1- Download Unity Interception using NuGet.

2- Create a custom attribute

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CacheAttribute : Attribute
{
    public double AbsoluteExpiration { get; private set; }

    public CacheAttribute(double absoluteExpiration)
    {
        AbsoluteExpiration = absoluteExpiration;
    }
}

3- Create  Interception Behavior

public class CachingInterceptionBehavior : IInterceptionBehavior
{
    private object _LockSync = new object();

    public bool WillExecute
    {
        get
        {
            return true;
        }
    }

    public CachingInterceptionBehavior() { }

    public IEnumerable GetRequiredInterfaces()
    {
        return Type.EmptyTypes;
    }

    public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
    {
        IMethodReturn result = null;

        CacheAttribute cacheAttr = input.Target.GetType().GetMethods().FirstOrDefault(m => m.Name == input.MethodBase.Name)
            ?.GetCustomAttributes(typeof(CacheAttribute), false).FirstOrDefault() as CacheAttribute;

        if (cacheAttr != null)
        {
            result = GetItem(input.MethodBase.Name, false) as IMethodReturn;
            if (result == null)
            {
                result = InvokeSource(input, getNext);
                if (result.Exception == null)
                    AddItem(input.MethodBase.Name, result, cacheAttr.AbsoluteExpiration);
            }
        }

        if (result == null)
            result = InvokeSource(input, getNext);

        return result;
    }

    private IMethodReturn InvokeSource(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
    {
        return getNext()(input, getNext);
    }

    private void AddItem(string key, object value, double seconds)
    {
        lock (_LockSync)
        {
            MemoryCache.Default.Add(key, value, DateTimeOffset.Now.AddSeconds(seconds));
        }
    }

    private void RemoveItem(string key)
    {
        lock (_LockSync)
        {
            MemoryCache.Default.Remove(key);
        }
    }

    private object GetItem(string key, bool remove)
    {
        lock (_LockSync)
        {
            var res = MemoryCache.Default[key];

            if (res != null)
            {
                if (remove == true)
                    MemoryCache.Default.Remove(key);
            }

            return res;
        }
    }
}

4- Register both the Interception extension and the new Cache behavior

Container.AddNewExtension()
         .RegisterType(new ContainerControlledLifetimeManager());

5- That’s it. It’s time to start decorating your functions

[Cache(absoluteExpiration: 60)]
public async Task GetValue()
{
  .....
}

 

WPF – Display .rft files with RichTextBox using MVVM

1- Xaml


<RichTextBox local:RichTextBoxHelper.DocumentContent="{Binding DocContent}"/>

2- Attached Properties


    public class RichTextBoxHelper : DependencyObject
    {
        public static string GetDocumentContent(DependencyObject obj)
        {
            return (string)obj.GetValue(DocumentContentProperty);
        }

        public static void SetDocumentContent(DependencyObject obj, string value)
        {
            obj.SetValue(DocumentContentProperty, value);
        }

        // Using a DependencyProperty as the backing store for DocumentContent.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DocumentContentProperty =
            DependencyProperty.RegisterAttached("DocumentContent", typeof(string), typeof(RichTextBoxHelper),
                new FrameworkPropertyMetadata
                {
                    BindsTwoWayByDefault = true,
                    PropertyChangedCallback = (obj, e) =>
                    {
                        var richTextBox = (RichTextBox)obj;

                        // Parse the XAML to a document (or use XamlReader.Parse())
                        var xaml = GetDocumentContent(richTextBox);
                        var doc = new FlowDocument();
                        var range = new TextRange(doc.ContentStart, doc.ContentEnd);

                        using (var reader = new MemoryStream(Encoding.UTF8.GetBytes(xaml)))
                        {
                            reader.Position = 0;
                            richTextBox.SelectAll();
                            richTextBox.Selection.Load(reader, DataFormats.Rtf);
                        }
                    }
                });
    }

3- ViewModel


    public class ViewModel
    {
        public string DocContent { get; set; }

        public ViewModel()
        {
            DocContent = File.ReadAllText(@"C:\sample.rtf");
        }
    }

WPF- How to traverse the Visual Tree

How to traverse the Visual Tree to find a control by name


public static T FindChild(DependencyObject parent, string childName)
    where T : DependencyObject
{
    if (parent == null)
        return null;

    T foundChild = null;

    int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
    for (int i = 0; i < childrenCount; i++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(parent, i);

        T childType = child as T;

        if (childType == null)
        {
            foundChild = FindChild(child, childName);
            if (foundChild != null)
                break;
        }
        else if (!string.IsNullOrEmpty(childName))
        {
            FrameworkElement frameworkElement = child as FrameworkElement;

            if (frameworkElement != null && frameworkElement.Name == childName)
            {
                foundChild = (T)child;
                break;
            }
        }
        else
        {
            foundChild = (T)child;
            break;
        }
    }

    return foundChild;
}

How to Find the first parent of a specific type


public static T FindFirstParent(DependencyObject child)
    where T : DependencyObject
{
    if (child == null)
        return null;

    DependencyObject firstParent = null;
    DependencyObject parent = child;
    while (parent != null)
    {
        if (parent is T)
        {
            firstParent = parent;
        }

        parent = VisualTreeHelper.GetParent(parent);
    }

    return firstParent as T;
}

WPF: Timeline control

I had a requirement to implement a timeline control that should somehow resembles the Facebook profile control, and I found it very interesting to implement it from scratch.

I am going to discuss a simplified version of this control. The following figure demonstrates the final look

Timeline

The Idea

It’s so simple. I used a Treeview and a ListBox to achieve the job. I am going to discuss both of them in details, but I want to give a glimpse on how those controls are populated with data.

The Data provider

The control uses a data provider to provide both the Treeview and the ListBox with data comes from the view model through two methods


public interface ITimelineDataProvider
{
    IEnumerable GetMatches(HierarchyData filter);
    IEnumerable GetFilters();
}

The GetFilters method provides the Treeview with filters, so the ListBox could filter out its data according to the Treeview selection.

The GetMatches methods accepts a filter parameter to filter out the data source and assign it to the ListBox.

The TreeView

This controls is responsible to populate years and months (You can change it to whatever you want) on a show/collapse fashion. The control uses a HierarchyData object to track the data level, children and other needed information.


public class HierarchyData : INotifyPropertyChanged
{
    #region Fields
    private bool _IsSelected;
    private bool _IsExpanded;
    #endregion

    #region Properties
    public HierarchyData Parent { get; set; }

    public object Value { get; set; }

    public IEnumerable Children { get; set; }

    public int Level { get; set; }

    public bool HasChildren
    {
        get
        {
            return Children?.Any() ?? false;
        }
    }

    public bool IsSelected
    {
        get { return _IsSelected; }
        set
        {
            _IsSelected = value;
            OnPropertyChanged("IsSelected");
        }
    }

    public bool IsExpanded
    {
        get { return _IsExpanded; }
        set
        {
            _IsExpanded = value;
            OnPropertyChanged("IsExpanded");
        }
    }
    #endregion

    #region INotifyPropertyChanged Implementation
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    #endregion
}

The ListBox

This control is responsible to populate the business object. The associated DataTemplate is given through the main project not by the control itself for re-usability reason. This Test application provides a simple DataEntity object


public class DataEntity
{
    public DateTime Date { get; set; }
    public string Message { get; set; }
}

Download

You can download the control along with a simple test application throughout this link Timelinecontrol.zip (Please remove the .pdf extension to be able to unzip it).

Thank you for reading my post. I hope you enjoyed my control. Please feel free to ask me questions. I will be more than happy to help.

Happy Coding!!!!