Jan 242012
 

Reusability is one of the most underrated aspects of software development. The focus on meeting functional requirements in a product as quickly as possible often overshadows the reusability and maintainability part. Code duplication, poor adherence to design are often symptoms of such development which increase costs in the long term in favour of short term benefits.

WPF encourages creation of reusable controls, primarily by its support for creation of “lookless controls” where the client of the control is not only able to reuse the control, but also completely alter its appearance. The control in essence is just a bundle of logic with a default template. Its upto the client whether to accept this default skin or override it with one of his own. Though this concept looks similar to themes and skins in other technologies, its extremely powerful that you can alter the visual appearance of the control at a granular level. If the default template defines a button and a textbox, that can easily be changed to an filled rectangle with a Linear Gradient and a TextBlock to display the data.

Difference between User Controls and Custom Controls.

Custom controls arent the only type of control library that WPF offers. There is also the User Control library. The difference between the two lies in our end purpose. If we are simply looking to bundle a few controls together and provide basic customization to the end user, then User Controls are the way to go. They are also much more easier to use than Custom Controls. However, if your aim is to provide full customization capability to the developer who consumes your control, then Custom Controls are much better.

Dependency Properties

Dependency Properties are quite different from the conventional properties which we use in C#, and are exclusive to WPF. They do not belong to any particular class and their value can be set sources other than just the class itself. The values could come from their default values, styles, themes, callbacks etc. They also support Data binding so your UI elements can directly bind to them and update whenever the value changes, like how properties in classes implementing INotifyPropertyChanged events behave.

Custom Control Example

The custom control example I build is a filled rectangle with a slider. As you increase the slider the fill percentage of the rectangle increases as well. Here is an illustration

The control is quite simple. There are two main properties here which are exposed to the outer world – The FillColor and the EmptyColor denoting the colors of the rectangle. The third property is the Value of slider which is used within the control, but that too could be exposed out of the control. Lets see the code for the control. ( Notice the absence of any UI stuff)

public class FilledBarControl : Control
    {
        public FilledBarControl()
        {
            DataContext = this;
        }
        
        static FilledBarControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(FilledBarControl), new FrameworkPropertyMetadata(typeof(FilledBarControl)));
          
        }

        public static readonly DependencyProperty EmptyColorProperty = DependencyProperty.Register("EmptyColor", typeof(Color), typeof(FilledBarControl), new UIPropertyMetadata((Color)Colors.Transparent));

        public Color EmptyColor
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (Color)GetValue(EmptyColorProperty);
            }
            set
            {
                SetValue(EmptyColorProperty, value);
            }
        }
        

        public static readonly DependencyProperty FillColorProperty = DependencyProperty.Register("FillColor", typeof(Color), typeof(FilledBarControl), new UIPropertyMetadata((Color)Colors.Red));

        public Color FillColor
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (Color)GetValue(FillColorProperty);
            }
            set
            {
                SetValue(FillColorProperty, value);
            }
        }
    }

As you can see, there are two steps to declaring a dependency property – First is the registering of the property using the DependencyProperty.Register method and the second is the definition of the getter and setter methods. The naming convention for the DependencyObject is to append “Property” after the name of the Dependecy Property. Hence here the object becomes FillColorProperty. You can also define the default values for the Property in the Register method. It needs to be passed inside the constructor of the PropertyMetadata object. I used Red Color here, so if the developer doesnt pass any value for the FillColor, the control automatically chooses Red.

Like written before, a custom control is simply defined in code, it doesn’t necessarily need a UI to exist – the UI can be supplied by the developer using the control. The default look and feel for the control is defined in a separate file – Generic.xaml inside the themes folder. Here is the xaml for the filled Bar control.

<Style TargetType="{x:Type local:FilledBarControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:FilledBarControl}">
                    <UniformGrid Columns="1">
                        <Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" >                           
                            <Rectangle Height="{TemplateBinding Height}"
                            Width="{TemplateBinding Width}">
                            <Rectangle.Fill>
                                <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                                    <GradientStop Color="{Binding Path=FillColor}"
                        Offset="0"/>
                                    <GradientStop Color="{Binding Path=FillColor}"
                        Offset="{Binding ElementName=slider, Path=Value}"/>
                                    <GradientStop Color="{Binding Path=EmptyColor}" 
                        Offset="{Binding ElementName=slider, Path=Value}"/>
                                </LinearGradientBrush>
                            </Rectangle.Fill>
                        </Rectangle>
                        </Border>

                        <Slider x:Name="slider" Width="200" Height="50" 
            Minimum="0" Maximum="1" Value="0.2"/>
                    </UniformGrid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

The XAML is just a control template containing a Uniform Grid. One of the rows is the Rectangle and the other a Slider for the value. The rectangle’s fill tag is a LinearGradientBrush with two phases. One from the Start Point (0,0) to the Value (bound to the Slider’s value) and another which starts from the Value to the end point. This gradient gives the impression of a filled rectangle whose fill percentage changes as the slider is dragged.

Now, how can a host application change the appearance of the control? There are two ways – one using the Dependency Properties and other completely overriding the Control Template itself. As you can recall, there were two dependency Properties defined in the FilledBar.cs – The FillColor and the EmptyColor. Both these properties appear in the Intellisense while defining the control in the XAML. An example of such customization would be

         <FilledBar:FilledBarControl HorizontalAlignment="Center" 
                                    VerticalAlignment="Center"  
                                    Height="100" Width="200" 
                                    BorderThickness="2" BorderBrush="#003300" 
                                    FillColor="Maroon" EmptyColor="LightGreen" />

This is how the control now looks. Note that both colours have changed as per our definition

The second form of customization is what makes Custom Controls so much more powerful than ordinary User Controls. Lets assume that a rectangular fill bar doesnt suit my requirement and my application would look better with a FilledCircle rather than a FilledBar. Rather than writing the entire control again just for one change, I could just swap out the Rectangle and substitute it with an Ellipse whose Fill is done by a RadialGradientBrush. These changes do not require any changes to the original control itself – a style can be created in the Resource Dictionary and referred to by the code.

            <Style TargetType="{x:Type FilledBar:FilledBarControl}" x:Key="FilledCircle">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type FilledBar:FilledBarControl}">
                            <UniformGrid Columns="1" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" >

                                <Ellipse Height="100" Width="100" Stroke="{TemplateBinding BorderBrush}" StrokeThickness="{TemplateBinding BorderThickness}"
                            >
                                        <Ellipse.Fill>
                                            <RadialGradientBrush >
                                                <GradientStop Color="{Binding Path=FillColor}"
                        Offset="0"/>
                                                <GradientStop Color="{Binding Path=FillColor}"
                        Offset="{Binding ElementName=slider, Path=Value}"/>
                                                <GradientStop Color="{Binding Path=EmptyColor}" 
                        Offset="{Binding ElementName=slider, Path=Value}"/>
                                            </RadialGradientBrush>
                                        </Ellipse.Fill>
                                    </Ellipse>
                              

                            <Slider x:Name="slider" Width="200" Height="50" 
            Minimum="0" Maximum="1" Value="0.2"/>
                        </UniformGrid>
                        </ControlTemplate>

                    </Setter.Value>
                </Setter>
                
            </Style>

The only changes in the above style tag are the changing to ellipse and RadialGradientbrush. Note that instead of using the Border, we just bind the BorderBrush and BorderThickness properties to the Stroke and StrokeThickness properties of the Ellipse. This allows setting of the properties just as we did with the unchanged controls and is much more easier to read. The control is declared in xaml with the style tag referring to our user dictionary which overrides the default style written in the Generic.xaml

        <FilledBar:FilledBarControl HorizontalAlignment="Center" VerticalAlignment="Center" 
                                    Grid.Row="3" Grid.Column="1"  
                                    Style="{StaticResource ResourceKey=FilledCircle}"  
                                    Height="200" Width="200" 
                                    BorderThickness="2" BorderBrush="Brown" 
                                    FillColor="Blue" EmptyColor="Transparent">

Here is how the control looks now.

We can have multiple instance of the same control using different styles and values for Dependency Property. An example of the Host Application

Jan 202012
 

I wrote a small application to extract post and user data for groups in the popular enterprise microblogging portal, Socialcast. It lets users access data instantly about the activity going on in their groups and the number of likes/comments being posted by group members.

The details about the API can be found here and their side demo.socialcast.com can be used for testing. There are three main calls in the application

  • Message Data for the group (Using the parameters Group_Id and since to filter by the data)
  • To find the group Id using the group name using the Groups.xml API call.
  • Then finding the group members using groupname/members.xml.

The method to retrieve group_id from the groupname needed improvement. Most organizations contain upwards of a 1000 groups. Making two heavy API calls for finding out the group id from the name is a bit of a drag on the performance of the application. To avoid this, I just cached all the group Ids and names in a Dictionary<string, int> object and used that to lookup the id. For groups that were created after the tool was distributed, the API call was a fallback.

The UI of the tool is fairly simple. Just a textbox to enter the group URL and two datagridviews to display the data along with two listboxes to display the stats. Since data retrieval is a very slow process, a backgroundworker with a progress bar make sure the application doesnt freeze and the user is shown some progress.  There is also a dropdown to select the time for which the user wants to retrieve messages and whether the member information is required or not. The quickest options are selected by default.


The API access code is in a library which hides the webservice calls from the client application. Each method contains the standard page and number of records per page argument. Apart from them, there are other parameter which are needed to make the call e.g GroupName, member id etc. To simply the URL construction for the web service call a class contains the skeleton urls for each call like given below

public static class ObjectType
 {
 static string defaultFormat = ".xml";
 public static string Users = "users" + defaultFormat;
 public static string Streams = "streams" + defaultFormat;
 public static string Messages = "messages" + defaultFormat;
 public static string StreamMessages = "streams/{0}/messages" + defaultFormat;
 public static string MessagesById = "messages/{0}" + defaultFormat;
 public static string SearchUsers = "users/search" + defaultFormat;
 public static string Groups = "groups" + defaultFormat;
 public static string GroupMembers = "groups/{0}/members" + defaultFormat;
 }

public class SocialCastData
 {
 ///
<summary> /// These are the private variables which are configured
 /// as per your socialcast site
 /// </summary>
 string skeletonURL = "https://{0}.socialcast.com/api/{1}";
//Rest of code here
}

The method to construct the service url takes in the Object Type member, and appends it to the skeleton url along with filling in the domain of the socialcast site and any additional parameter (e.g. group id). This approach allows me to add new service calls easily with just one more variable in the ObjectType class. Any additional query string parameters are supplied using the serviceParams which is a list of keyvaluepairs. The list is iterated through and appended after the url. The SocialcastAuthDetails object is a must in every calls since it contains the domain, the username and password all of which are required to be supplied for getting the response.

Here is the method to get the Group ID from the groupName. The GroupQuery.QueryForGroupId accesses the cache and returns immediately if found. If not then another API call is made to get all the groups, iterate through them and identify the id. If the group id is still not found, an exception is thrown.

        private int GetGroupIdByGroupName(string groupName,SocialCastAuthDetails auth)
        {

            int _groupId = 0;
            int _pageNumber =1;
            bool moreItems = true;

            _groupId = GroupsQuery.QueryForGroupID(groupName);
            if (_groupId != 0)
                return _groupId;
            else
            {

                while (moreItems)
                {
                    XmlDocument group = new XmlDocument();
                    var serviceParams = new List>();
                    serviceParams.Add(new KeyValuePair("page", _pageNumber.ToString()));
                    serviceParams.Add(new KeyValuePair("per_page", "500"));
                    group.LoadXml(base.MakeServiceCalls(helper.GetSocialcastURL(ObjectType.Groups, auth.DomainName, null, serviceParams),GetCredentials(auth.Username,auth.Password)));
                    if (group.SelectNodes("//groups/group") == null || group.SelectNodes("//groups/group").Count == 0)
                        moreItems = false;
                    else
                    {
                        foreach (XmlNode groupNode in group.SelectNodes("//groups/group"))
                        {
                            if (String.Compare(GetNodeInnerText(groupNode, "groupname"), groupName, true) == 0)
                            {
                                string id = GetNodeInnerText(groupNode, "id");
                                _groupId = Convert.ToInt32(id);
                                GroupsQuery.AddToDictionary(groupName, _groupId);
                                moreItems = false;
                                break;
                            }
                        }
                    }
                    _pageNumber++;
                }
                return _groupId;
            }
        }

In the front end, a background worker is used to perform the time consuming operation asynchronously. Here is the code (Rough, not refactored)

    class GetStatistics
    {
        APIAccessor _api = new APIAccessor();

        public AppData GetData(string urlEnteredbyUser, string sinceWhatPeriod, bool loadUserData)
        {
            var serviceUrl = UrlReplace(urlEnteredbyUser.Trim());
            List<SocialcastMessage> messages = null;
            List<UserProfile> users = null;
            if (serviceUrl == string.Empty)
            {
                throw new GroupNotFoundException(null, "The group Url you have entered is not valid");
            }
            else
            {
                messages = GetMessageStats(serviceUrl, sinceWhatPeriod);
                if(loadUserData)
                        users = GetAllGroupUsers(serviceUrl);
                return new AppData(messages, users);
            }
        }

        private List<UserProfile> GetAllGroupUsers(string groupName)
        {
            List<UserProfile> Users = new List<UserProfile>();
            int userCount = 500;
            int pageNumber = 1;
            bool moreItems = true;
            while (moreItems)
            {
                var xdoc = _api.GetGroupMembers(groupName, pageNumber.ToString(), userCount.ToString(), GetCredentials());
                if (xdoc.SelectNodes("//users/user") == null || xdoc.SelectNodes("//users/user").Count == 0)
                    moreItems = false;
                else
                {
                    foreach (XmlNode userNode in xdoc.SelectNodes("//users/user"))
                    {
                        string name = GetNodeInnerText(userNode, "name");
                        string userName = GetNodeInnerText(userNode, "username");
                        string email = GetNodeInnerText(userNode, "contact-info/email");
                        int followingCount = Convert.ToInt32(GetNodeInnerText(userNode, "following-count"));
                        int followersCount = Convert.ToInt32(GetNodeInnerText(userNode, "followers-count"));

                        Users.Add(new UserProfile()
                        {
                            Name = name,
                            userName = userName,
                            Email = email,
                            FollowerCount = followersCount,
                            FollowingCount = followingCount
                        });
                       
                    }
                    pageNumber++;
                }
            }

            return Users;
        }

        private List<SocialcastMessage> GetMessageStats(string serviceUrl, string sinceWhatPeriod)
        {
            List<SocialcastMessage> Messages = new List<SocialcastMessage>();
            int messageCount = 500;
            int pageNumber = 1;
            bool moreItems = true;
            long sinceWhen = GetSinceWhenValue(sinceWhatPeriod);
            while (moreItems)
            {
                var xdoc = GetMessagesForTheGroup(serviceUrl, messageCount, pageNumber, sinceWhen);
                if (xdoc.SelectNodes("//messages/message") == null || xdoc.SelectNodes("//messages/message").Count == 0)
                    moreItems = false;
                else
                {
                    foreach (XmlNode node in xdoc.SelectNodes("//messages/message"))
                    {
                        DateTime createdDate;
                        DateTime.TryParse(GetNodeInnerText(node, "created-at"), out createdDate);
                        string ticksLastUpdatedDate = GetNodeInnerText(node, "last-interacted-at");
                        var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
                        epoch = epoch.AddSeconds(Int64.Parse(ticksLastUpdatedDate));
                        string permaUrl = GetNodeInnerText(node, "permalink-url");
                        string commentsCount = GetNodeInnerText(node, "comments-count");
                        string likesCount = GetNodeInnerText(node, "likes-count");
                        string title = GetNodeInnerText(node, "title");
                        string user = GetNodeInnerText(node, "user/name");

                        Messages.Add(new SocialcastMessage()
                        {
                            Title = title,
                            Url = permaUrl,
                            CreatedDate = createdDate,
                            UpdatedDate = epoch,
                            Comments = int.Parse(commentsCount),
                            Likes = int.Parse(likesCount),
                            UserName = user
                        });

                    }
                    pageNumber++;
                }
            }


            return Messages;

        }


        private long GetSinceWhenValue(string sinceWhatPeriod)
        {
            switch (sinceWhatPeriod)
            {
                case "All Messages":
                    return 0;
                case "Since Last week":
                    return GetTicksSince1970(7);
                case "Since Last Month":
                    return GetTicksSince1970(30);
                default:
                    return 0;
            }
        }

        private static long GetTicksSince1970(int days)
        {
            DateTime sinceLastWeek = DateTime.Now.Subtract(new TimeSpan(days, 0, 0, 0, 0));
            DateTime epoch = new DateTime(1970, 1, 1);
            return Convert.ToInt64(sinceLastWeek.Subtract(epoch).TotalSeconds);
        }


        private XmlDocument GetMessagesForTheGroup(string streamName, int numberOfPosts, int page, long sinceWhen)
        {
            string sinceWhenString = null;
            if (sinceWhen != 0)
                sinceWhenString = sinceWhen.ToString();
            var xdoc = _api.GetStreamMessages(streamName, numberOfPosts.ToString(), page.ToString(), sinceWhenString, GetCredentials());
           return xdoc;
        }


        private string UrlReplace(string enteredByUser)
        {
        
            string regexPattern = @"https://demo.socialcast.com/groups/([A-Za-z0-9\-]+)";
            Match match = Regex.Match(enteredByUser, regexPattern,
                RegexOptions.IgnoreCase);

            // Here we check the Match instance.
            if (match.Success)
            {
                // Finally, we get the Group value and return it
                string key = match.Groups[1].Value;
                return key;
            }
            return string.Empty;
        }

        private SocialCastAuthDetails GetCredentials()
        {
            return new SocialCastAuthDetails()
            {
                Password = "demo",
                Username = "emily@socialcast.com",
                DomainName = "demo"
            };

        }

        private string GetNodeInnerText(XmlNode node, string xpath)
        {
            try
            {
                if (node.SelectSingleNode(xpath) != null)
                {
                    return node.SelectSingleNode(xpath).InnerText.Trim();
                }
                return String.Empty;
            }
            catch
            {
                return string.Empty;
            }
        }
    }