(X) Hide this
    • Login
    • Join
      • Say No Bots Generate New Image
        By clicking 'Register' you accept the terms of use .

Silvester - A Silverlight Twitter Widget

(18 votes)
Emil Stoychev
>
Emil Stoychev
Joined Oct 15, 2007
Articles:   23
Comments:   98
More Articles
42 comments   /   posted on Jun 25, 2008
Tags:   twitter , emil-stoychev
Categories:   General , Controls

This article is compatible with the latest version of Silverlight.

Are you a fan of Twitter? Personally I'm, but I'm also a fan of Silverlight. Twitter has a couple of Flash and HTML badges (a.k.a widgets) you can get and put on your blog to let your visitors know what you are up to in the moment. However, Twitter does not have a Silverlight widget. What a negligence! :) If you are like me and want not a Flash, but a Silverlight widget on your blog go ahead and read on how you can build one by yourself or just copy the text below to use it.


Copy this HTML snippet, replace the twitterUser init parameter with your Twitter username and put it in your blog to let your visitors see your tweets.

UPDATE:
In this version you can also specify color scheme though the initParams parameter. The default values are as follows:

<param name="initParams" value="twitterUser=silverlightshow,
fontSize=9,
linkColor=#FFFFFF,
textColor1=#C5C5C5,
textColor2=#FFFFFF,
backgroundColor=#000000,
tweetBackgroundColor=#343434,
tweetSelectedBackgroundColor=#515151"
/>

Color scheme parameters: 

key description
fontSize font size
linkColor link color
textColor1 text color
textColor2 time gone, loading text, refresh button text
backgroundColor background color
tweetBackgroundColor background color of the status items
tweetSelectedBackgroundColor background color of the selected status

Download Source Code
 

Why Silvester?

Because it's funny. Remember the Warner Bros' Tweety right? Not sure if it has something to do with Twitter, but it does sounds the same way to me. So in the cartoon there is also a cat named Sylvester that want to eat the bird (Tweety). Thinking about Twitter, Tweety, Sylvester and Silverlight we came up with Silvester :)

Ok, enough laughing, let's start building the widget.

The UI

The Silvester's UI is pretty much simple. Basically what we need is a ListBox control that contains the tweets (user statuses).

 Tweet 
Structure of a tweet - user profile image, username, text, time gone

To arrange the elements I use a two StackPanels - one with Horizontal orientation to position the Image and the tweet details and one with Vertical orientation to position the username, the status and the time.

 "{StaticResource Status}" >
     
         Source="{Binding User.ProfileImageUrl}"
         Style="{StaticResource UserProfileImage}"   />
     "{StaticResource StatusData}">
         
             Content="{Binding User.Name}"
             NavigateUri="{Binding User.Url}"
             Style="{StaticResource UserName}"   />
          
              Text="{Binding Text}"
              TextStyle="{StaticResource LinkLabelText}"
              LinkStyle="{StaticResource LinkLabelLink}"
              Style="{StaticResource StatusText}"   />
          
              Text="{Binding TimeGone}"
              Style="{StaticResource StatusTimeGone}"   />
       

Fine, but what is this LinkLabel control? This is a custom control that represents a rich TextBlock with the ability to contain hyperlinks. For more information about it please read my previous article.

The UI is not complicated; however there are some elements, like the scrollbars, that need special attention. I won't get any deeper on this topic here, but we plan to write another article that will help you understand how you can easily customize elements using the States and Parts Model with VisualStateManager. If you are interested in the States and Parts Model you can read the great 4 parts tutorials by Karen Corby and Animating ListBoxItems - the VisualStateManager.

The Code

The main task of Silvester is to get the user timeline, i.e. the recent user posts. We add one more feature - refresh the timeline on a predefined interval of time.

Twitter service is available through a public API. However, to make calls from Silverlight to their API they have to explicitly add your domain to allow this (read more on the configuring a web service to enable Silverlight callers here). Of course, that's not an option for us - we want just to put the widget on our blog and have everything work, without having to ask Twitter to add our domain to their clientaccesspolicy.xml file (even if you ask them, like we did, they won't mind).

So what we do to get over this problem? We create a proxy service, hosted on our domain, which we will call in order to reach Twitter.

To achieve our goals we will follow several steps:

  1. Create a proxy service to get the user timeline from the Twitter API
  2. Reference the service from the Silverlight project
  3. Create a timer to update the timeline every minute

Proxy Service to Get the User Timeline

Before creating the service let's see what kind of business object we need. In the UI we display a few fields - user profile image, username (with a hyperlink to the Twitter account), status and time gone. To reflect the XML structure of the user timeline that we receive from the Twitter API we create 2 business objects - Status and User. Let's first take a look at the XML structure.

 "array">
     
         Tue Jun 24 16:11:02 +0000 2008
         842567566
         Tip: Asynchronous Silverlight - Execute on the UI thread http://tinyurl.com/5o2rp9
         web
         false
         
         
          false
          
              14341499
              silverlightshow
              silverlightshow
              Sofia, Bulgaria
              SilverlightShow.net - Silverlight articles, tutorials, showcase, videos
              http://s3.amazonaws.com/twitter_production/profile_images/52594162/estoychev_bigger_normal.png
              http://www.silverlightshow.net/
              <protected>falseprotected>
              29
          
      
  ...

We display only a part of this information so the business objects will reflect only those elements that need to be displayed.

Status

 [Serializable]
 public class Status
 {
     public string CreatedAt
     {
         get;
         set;
     }
 
      public string Text
      {
          get;
          set;
      }
  
      public User User
      {
          get;
          set;
      }
  
      public string TimeGone
      {
          get
          {
              string[] values = this.CreatedAt.Split( ' ' );
              string timeValue = string.Format( "{0} {1}, {2} {3}", values[ 1 ], values[ 2 ], values[ 5 ], values[ 3 ] );
              DateTime parsedDate = DateTime.Parse( timeValue );
  
              DateTime relativeTo = DateTime.Now;
  
              // time difference in seconds
              double delta = relativeTo.Subtract( parsedDate ).TotalSeconds + DateTime.UtcNow.Subtract( relativeTo ).TotalSeconds;
   
              if ( delta < 60 )
              {
                  return "less than a minute ago";
              }
              else if ( delta < 120 )
              {
                  return "about a minute ago";
              }
              else if ( delta < ( 60 * 60 ) )
              {
                  return ( int )( delta / 60 ) + " minutes ago";
              }
              else if ( delta < ( 120 * 60 ) )
              {
                  return "about an hour ago";
              }
              else if ( delta < ( 24 * 60 * 60 ) )
              {
                  return string.Format( "about {0} hours ago", ( int )( delta / 3600 ) );
              }
              else if ( delta < ( 48 * 60 * 60 ) )
              {
                  return "1 day ago";
              }
              else
              {
                  return ( int )( delta / 86400 ) + " days ago";
              }
          }
  
          set
          {
  
          }
      }
  }

User

 [Serializable]
 public class User
 {
     public string Name
     {
         get;
         set;
     }
 
      public string ProfileImageUrl
      {
          get;
          set;
      }
 
      public string Url
      {
          get;
          set;
      }
  }

It's finally time to make the proxy service. Let's first see the code and then I'll give some explanations:

 [System.Web.Script.Services.ScriptService]
 public class TwitterWebService : System.Web.Services.WebService
 {
     private const string UserTimelineUri = "http://twitter.com/statuses/user_timeline/{0}.xml";
     private const string StatusElementName = "status";
     private const string CreatedAtElementName = "created_at";
     private const string TextElementName = "text";
     private const string ProfileImageUrlElementName = "profile_image_url";
     private const string NameElementName = "name";
     private const string UserElementName = "user";
     private const string UserUrlElementName = "url";
      public const int RequestRateLimit = 70;
  
      [WebMethod]
      public List GetUserTimeline( string twitterUser, string userName, string password )
      {
          if ( string.IsNullOrEmpty( twitterUser ) )
          {
              throw new ArgumentNullException( "twitterUser", "twitterUser parameter is mandatory" );
          }
  
          WebRequest rq = HttpWebRequest.Create( string.Format( UserTimelineUri, twitterUser ) );
  
          if ( !string.IsNullOrEmpty( userName ) && !string.IsNullOrEmpty( password ) )
          {
              rq.Credentials = new NetworkCredential( userName, password );
          }
  
          HttpWebResponse res = rq.GetResponse() as HttpWebResponse;
  
          // rate limit exceeded
          if ( res.StatusCode == HttpStatusCode.BadRequest )
          {
              throw new ApplicationException( "Rate limit exceeded" );
          }
  
          XDocument xmlStatusData = XDocument.Load( XmlReader.Create( res.GetResponseStream() ) );
          List data = ( from status in xmlStatusData.Descendants( StatusElementName )
                                select new Status
                                {
                                    CreatedAt = status.Element( CreatedAtElementName ).Value.Trim(),
                                    Text = status.Element( TextElementName ).Value.Trim(),
                                    User = new User()
                                    {
                                        ProfileImageUrl = status.Element( UserElementName ).Element( ProfileImageUrlElementName ).Value.Trim(),
                                        Name = status.Element( UserElementName ).Element( NameElementName ).Value.Trim(),
                                        Url = status.Element( UserElementName ).Element( UserUrlElementName ).Value.Trim()
                                    }
                                } ).ToList();
          return data;
      }
  }

The web service contains only one method - GetUserTimeline. Basically it just creates a new WebRequest and loads the underlying response stream into an XDocument. Then a new generic list of type Status is created and initialized using LINQ to XML. Nothing special here - it is a normal web service you all have used/built. A couple of constants are defined just before the web method to define the API method address and to simplify the XML work.

Reference the Service

Add a reference to the service we've just created by using the Add Reference context menu item of the Silverlight project. Get the desired user's timeline:

 private void RefreshTimeline( object sender, EventArgs e )
 {
     TwitterWebServiceSoapClient twitterClient = new TwitterWebServiceSoapClient();
     twitterClient.GetUserTimelineCompleted +=
         new EventHandler( this.GetUserTimelineCompleted );
     twitterClient.GetUserTimelineAsync( this.TwitterUser, null, null );
 }

On line 4 we pass a GetUserTimelineCompleted which is the target method to be called when the call completes. On line 5 this.TwitterUser is a property that specifies the username of the user whose timeline we want to get.

 private void GetUserTimelineCompleted( object sender, GetUserTimelineCompletedEventArgs e )
 {
     StatusList.ItemsSource = e.Result;
 }

In the callback just get the returned result and assign it to the ItemsSource property of the ListBox that contains the tweets.

Create a Timer to Update the Timeline

Twitter is obsessive. There are tweeters that updates their status every few minutes. To keep the widget up to date we need a mechanism to update the timeline every couple of minutes (by default every 3 minutes). To achieve this we can use a DispatcherTimer.

 private DispatcherTimer timelineTimer;
 private void RefreshTimeline()
 {
    // first refresh
    if ( this.timelineTimer == null )
    {
        this.timelineTimer = new DispatcherTimer();
        this.timelineTimer.Tick += new EventHandler( RefreshTimeline );
        this.timelineTimer.Interval = new TimeSpan( 0, 1, 0 );
    }
  
     // manual refresh
     else
     {
         this.timelineTimer.Stop();
     }
  
     this.RefreshTimeline( this, EventArgs.Empty );
  }

A couple of changes are needed in the GetUserTimelineCompleted callback to make it work properly with the DispatcherTimer.

  1. If an error is encountered during the web service call the timer should be stopped.
  2. If the timeline is explicitly refreshed by the user the timer should be restarted.
 private void GetUserTimelineCompleted( object sender, GetUserTimelineCompletedEventArgs e )
 {
     if ( e.Cancelled || e.Error != null )
     {
         this.timelineTimer.Stop();
         // process the error
         return;
     }
 
      StatusList.ItemsSource = e.Result;
  
      // restart the timer if the user explicitly
      // call the refresh action
      if ( !this.timelineTimer.IsEnabled )
      {
          this.timelineTimer.Start();
      }
  }

I've hidden the error processing and other code for clarification.

Summary

Building a Twitter widget is a straight and relatively easy task - depends on what UI you want. You can download the full source code and examine it for yourself. If you have any questions or misunderstandings please comment below and I'll be glad to clarify the things for you. 

Related Articles

Animating ListBoxItems - the VisualStateManager by Ivan Dragoev

Silverlight LinkLabel control by Emil Stoychev

Asynchronous Silverlight - Execute on the UI thread by Emil Stoychev

States & Parts Model with VisualStateManager by Karen Corby


Subscribe

Comments

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Jeff Blankenburg on Jun 25, 2008 16:04

    This is a great resource for the Silverlight community.  Thanks for sharing this!  I've been looking for a great example of Silverlight and Twitter working together.

    Jeff Blankenburg - Microsoft Developer Evangelist

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by timheuer on Jun 26, 2008 12:57

    i see that Twitter added you to their cross domain list...so you should be able to eliminate the proxy service now right?

  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on Jun 26, 2008 14:21

    Tim, not exactly... they've added our domain in the crossdomain.xml file - the Flash format. Unfortunately, to enable Silverlight applications to access services hosted on domains containing only crossdomain.xml, the policy file should be configured to allow access from any domain as described in MSDN. For now we should stick to the proxy service.

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by flowerpower on Jul 09, 2008 19:07

    Funny Silvester. haha. Made a geek's day a little lighter. haha

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by vasudev on Aug 14, 2008 01:13

    It opens the twitter page based on 'Name' in the Twitter settings and not on 'Username' when clicked on displayed name in Tweet. This has to be corrected or am I missing something?

     

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Offbeatmammal on Sep 26, 2008 14:07

    Have you thought about submitting this to http://apiwiki.twitter.com/Open+source ... would be awesome if the Twitter folks saw it and made it a standard offering alongside the Flash badge :)

  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on Sep 29, 2008 05:57

     Thank you for the idea Offbeatmammal. I'm planning some new features for the next week so may be then I can submit it.

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Offbeatmammal on Oct 10, 2008 13:36

    Any plans to update/re-release as a SL2 RTW version? I was using the RC0 bits and realised it's broken :(

    You can get them from http://silverlight.net to check

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by vasudev on Oct 15, 2008 04:20

    Yes its broken with SL2 RTW, please update Silvester.

  • Enrai

    RE: Silvester - A Silverlight Twitter Widget


    posted by Enrai on Oct 15, 2008 04:29

    Yes, Silvester will be converted as soon as possible to the Silverlight 2 Final version. :)

  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on Oct 15, 2008 07:04

    Sure, I'll update Silvester within the end of the week. Sorry, busy week :(

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by vasudev on Oct 21, 2008 06:13

    Its still not working, If I open this page I can see the Widget, but when code put in the blog, it just displays white patch. While I can right click on that white patch and get SL Configuration window.

    Any help on that, emil?

  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on Oct 21, 2008 06:28

    Yes, I know about that. There is a breaking change in Silverlight 2 RTW that validates the MIME type returned on the HTTP response is application/x-silverlight-app. Currently our hosting provider hasn't configured the IIS correctly and returns application/octet-stream. That's why the XAP file is only downloaded but the Silverlight doesn't run. I've already send a support ticket and I hope they fix behavior.

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Eric Kleeman on Nov 04, 2008 19:36

    Too bad none of the silverlight examples here work. I have the addon yet the examples just ask to download silverlight. MS needs to fix this to be serious abotu promoting this tool.

  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on Nov 18, 2008 02:40

    The widget can be used again! Since we moved to a new hosting environment there should be no problem to put the widget on your blog/site/etc. Enjoy!

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by vasudev on Nov 28, 2008 04:52

    Thanks emil, I have put it back now on my blog.

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by vasudevg on Nov 28, 2008 05:09

    Again its not showing tweet contents, Just showing the empty box. Have to check if its tweeter problem or Silvester's.

  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on Nov 28, 2008 06:44

    hm... I've try it on my blog: http://weblogs.asp.net/emilstoichev/archive/2008/06/26/silvester-silverlight-twitter-widget.aspx

    works perfect... Please let me know if you find something. Thank you.

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Justin Angel on Dec 26, 2008 01:06

    Thanks, I'm now using this on my official blog at Silverlight.Net: http://silverlight.net/blogs/justinangel.

    I've chosen to use this because it's the BEST twitter widget, not just because it's silverlight. Way to go folks!

    -- Justin Angel
    Microsoft SIlverlight Program Manager

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by vasudev on Jan 06, 2009 00:50

    Hi emil, Again today at present the tweets are not showing. Just displays the black background with 'Refresh'  , 'Follow Me' links.

     

  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on Jan 06, 2009 10:20

     Hi vasudev,

    I can see it is working now. I don't know what was going on - our service haven't been down - I guess it was something from Twitter.

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by vasudev on Jan 14, 2009 23:46

    I dont know emil, twitter works fine at the time when this happens. And its happening intermittently number of times almost daily. It just shows the background box with links, no tweet text. Just now its happening again and I cant get the tweets on this page also even after refreshing. Is it to do with the hosting server? Please check.

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Vlasta on Feb 07, 2009 12:08

    Hi, I can confirm that behavior. Only theframe and no feeds are being shown. It is the same on my own website, such as on the example shown here. It seems to be working randomly, because at first I'had seen feeds here, but they are not here anymore.

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Laurent on May 12, 2009 01:05
    Hi,

    I am curious to know how you have overcome the 100 requests per hour using a proxy service. Have you been whitelisted by Twitter? Are you caching requests? I'd really like to know...

    Thanks

     


  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on May 12, 2009 01:59
    Hi, Laurent. I do not overcome this issue. I wrote to Twitter when I started to write the client but my request wasn't approved. Maybe I should try again.
  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Laurent on May 13, 2009 15:59
    Hi again Emil.

     I believe your users are hitting the 100 requests per hour limit and this is why users are "complaining". I think the problem lies with authentication. I am currently working on this and I think I am close to understand what's going on... Will post again later.

    L.

     

     


  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Laurent on May 13, 2009 17:40

     Hello again,

    Here is below what is needed to properly authenticate with Twitter so that your server IP does not quickly hit the 100 rate limit. 

     

                    WebRequest rq = HttpWebRequest.Create(timelineUri);
                    // HttpWebRequest is a handy .NET class for doing HTTP requests. It has built-in support for HTTP basic
                    // authentication via credentials. However, it doesn’t work the way I expected: supplying credentials
                    // doesn’t send Authorization HTTP header with the request but only in response to server’s challenge.
                    // It often breaks in real world, where servers might not issue a challenge and simply not authenticate a request.
                    string up = userName + ":" + password;
                    CredentialCache cache = new CredentialCache();
                    cache.Add(new Uri(timelineUri), "Basic", new NetworkCredential(userName, password));
                    rq.Credentials = cache;
                    rq.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(new ASCIIEncoding().GetBytes(up)));
                    HttpWebResponse res = rq.GetResponse() as HttpWebResponse;
  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on May 14, 2009 01:47
    From the Twitter API Wiki:
    "The default rate limit for calls to the REST API is 100 requests per hour. The REST API does account- and IP-based rate limiting. Authenticated API calls are charged to the authenticating user's limit while unauthenticated API calls are deducted from the calling IP address' allotment."

    Yep, you seems to be right. The rate limit is both account and IP-based so when I apply your fix every user will have his/her 100 API calls per hour. That would be enough for most of the blogs.

    Thanks, Laurent!
  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on May 14, 2009 02:01
    Now the problem is how would the users pass their password. Obviously, it is not safe to pass it as a parameter in InitParams because this way everyone can read it. We use a proxy service so there is no guarantee for the users that we won't store their password. I have to think about that.
  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Laurent on May 14, 2009 16:27
    The widget should pop a dialog to enter credentials. Then you can save those in Isolated Storage for future reuse.
  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on May 15, 2009 02:58
    That's not an option as you put the widget on your blog and you want visitors to read your timeline, not theirs.
  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Mrathi on Sep 21, 2009 23:01

    Hi,

    I have downloaded your project and placed the ClientBin folder on my php website. Looks like it is not able to call the webservice to get the tweets. Can you please provide some information on how to use it in a non asp.net site?

    By the way, thanks for the wonderful project.

  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on Sep 23, 2009 08:32
    Hi Mrathi,

    I'm sorry to disappoint you but this widget may not work as expected all the time. Due to limitations in the Twitter service the widget can't serve more than 100 tweets per hour. There isn't anything special that you have to do to place the widget in a non asp.net site. You just have to copy the HTML above on a page in your web site. You don't have to copy any files.
  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Silas on Nov 19, 2009 06:47
    Thanks for sharing - really helpful!
  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by Fenil Desai on Oct 07, 2010 14:15

    It doesn't show the RT's but only the personal tweets.

    Would be gr8 if it shows RT's as well....

  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by kjkkkjjkkkkkkkkkkkkk on Feb 24, 2011 00:18

    mmmmmmmm

  • lnikolov

    RE: Silvester - A Silverlight Twitter Widget


    posted by lnikolov on Feb 28, 2011 15:01
    The article has been updated to the latest version of Silverlight and Visual Studio.
  • -_-

    RE: Silvester - A Silverlight Twitter Widget


    posted by silverlight novice on Mar 26, 2011 20:36

    Hello, I have opened the project in Visual Studio 2010. When running it it says call to webservice will fail UNLESS the silverlight project is launched from the web project that contains the webservice.

  • emil

    RE: Silvester - A Silverlight Twitter Widget


    posted by emil on Apr 27, 2011 09:57

    Set the web project as Startup Project and try to run it again.

     

    Emil

  • interlacecondo5

    Re: Silvester - A Silverlight Twitter Widget


    posted by interlacecondo5 on Mar 28, 2014 07:41
    A wonderful and unique lifestyle awaits you. Please see The Interlace project details and floor plans for more information. The Interlace at depot road singapore st 41
  • ctowers4

    Re: Silvester - A Silverlight Twitter Widget


    posted by ctowers4 on Apr 03, 2014 06:30
    Commonwealth Towers facilities provide full family entertainment needs for your family and loved ones. Indulge in a serene and tranquil lifestyle right in the heart of Queenstown commonwealth towers video
  • realestate4

    Re: Silvester - A Silverlight Twitter Widget


    posted by realestate4 on Apr 10, 2014 04:32
    To help some 2,800 heartland shops make their neighbourhoods more vibrant, $1.9 million has been set aside for the sixth round of the Housing Board's Revitalisation of Shops scheme. rents in office space

Add Comment

Login to comment:
  *      *