TL;DR:
We’re almost done with this series. Hallelujah? This one is about building the class that will display all of our feed contents and an HTML helper class that will help us build the HTML that goes into the WebBrowser control. Code is here, video is here.
Displaying the Feed Content
So again, a little copy paste action: Code is here, video is here (or you could scroll down a little more too)
There are only two classes we’re gonna add. DisplayManager.cs and HtmlWriter.cs
DisplayManager.cs
So this is most of the class. I’m going to leave out the LoadFeedInBrowser
function for now, but I’ll come back to it after I explain the HtmlWriter class.
class DisplayManager { private FeedsManager Feeds { get; } private WebBrowser Browser { get; } private MetroLabel TimeStamp { get; } public MetroTile SelectedTile { get; set; } public DisplayManager(FeedsManager feeds, WebBrowser browser, MetroLabel timeStamp) { Feeds = feeds; Browser = browser; TimeStamp = timeStamp; } public void AddTile(MetroPanel panel, MetroTile tile) { var numTiles = panel.Controls.OfType().Count(); tile.Location = new Point(numTiles * 155, 0); panel.Controls.Add(tile); } public MetroTile CreateTile(Feed feed) { MetroTile tile = new MetroTile(); tile.Text = feed.Name; tile.Style = MetroColorStyle.Purple; tile.TileTextFontSize = MetroTileTextSize.Tall; tile.UseStyleColors = true; tile.Theme = MetroThemeStyle.Dark; tile.Tag = feed; tile.Click += new EventHandler(LoadFeedInBrowser); tile.Size = new Size(150, 100); return tile; } public void UpdateTiles(MetroPanel panel) { UnloadTiles(panel); LoadTiles(panel); } public void LoadTiles(MetroPanel panel) { foreach (Feed feed in Feeds.GetFeedsFromConfig()) { var tile = CreateTile(feed); AddTile(panel, tile); } } public void UnloadTiles(MetroPanel panel) { SelectedTile = null; var tiles = panel.Controls.OfType().ToArray(); for (var i = tiles.Count() - 1; i >= 0; i--) { panel.Controls.Remove(tiles[i]); tiles[i].Dispose(); } panel.AutoScrollPosition = new Point(0, 0); } }
Translation:
DisplayManager()
takes a few paramaters. We need a list of the feeds we’re going to display, we’re going to need a reference to theWebBrowser
control that we’re going to load them into, and finally, we’ll need a reference to theMetroLabel
control that we’re going to update with a timestampAddTile()
takes aMetroPanel
and aMetroTile
, figures out where to place the tile on the panel (new Point(numTiles * 155, 0)
), and finally adds it to the panel.CreateTile()
needs aFeed
. This function handles all of the busy work associated with creating a newMetroTile
and sets theTag
as the givenFeed
. Several form controls in ASP.NET let us use theTag
property like this. It’s basically a handy little holder variable that we can point to whatever we want. We also register a click event for each tile that’s created. More on that later too.UpdateTiles()
lets us pass in anyMetroPanel
we want and then effectively refresh it by unloading everything and loading it back. This is handy for making changes to the feeds from the configuration side of things.LoadTiles()
loops through all of the feeds in our list, creates a tile from each one, and then adds it. (Remember what we passed in the constructor?)UnloadTiles()
does the exact opposite ofLoadTiles()
. Go figure. However, we also do a couple of other things here too. We have to set theSelectedTile
property tonull
AND reset the scroll position of the panel. If we deleted several tiles at once, we might run into a situation where the panel is scrolled out of range and from personal experience, that bug is not pretty. You can see what happens here.
HtmlWriter.cs
class HtmlWriter { private string html; private string css; public string HTML { get { return "<style>\n" + css + "\n</style>\n" + html; } } public HtmlWriter() { css = ""; html = ""; } public string CreatePairedTag(string tag, string classes, string contents) { var html = "<" + tag + " class=\"" + classes + "\">\n" + contents + "\n</" + tag + ">\n"; return html; } public string CreateLink(string href, string classes, string contents) { var link = "<a class=\"" + classes + "\" href=\"" + href + "\">" + contents + "</a>\n"; return link; } public string CreateSingleTag(string tag, string classes) { var html = "<" + tag + " class=\"" + classes + "\">\n"; return html; } public void AddTag(string tag) { this.html += tag; } public void AddCSSRule(string selector, string[] properties) { var css = selector + "{\n"; foreach (string property in properties) { css += property + ";\n"; } css += "}\n"; this.css += css; } }
All this class does is help us write our HTML tags. Instead of piecing together strings and then inserting c# variables and objects in funky places, we can pass them neatly to these methods and have it format everything for us. You could expand on this if you wanted to, but it serves it purpose pretty well in its current form.
Why do we need HTML anyway?
If you remember way back in the beginning of this series, we’re using a WebBrowser
control to display the all of the feed content. RSS Feeds already include HTML in their XML tags, so using that control just makes sense. However, we have to include some other formatting to make all of the RSS items look cohesive on a single page. That’s why we need to throw a little of our own HTML in the mix and this class is a clean way to do it.
- Our two variables,
html
andcss
just store our markup as we create it. - The property
HTML
combines thecss
and thehtml
strings together and sends us the full markup string. - The three
Create
functions all accomplish similar goals. They build html syntax around the variables we pass into them. We can even give them css classes too. - The
AddTag()
function just adds the passed tag to the html string. - The
AddCSSRule()
function is a little different. The concept is the same in that it builds the proper css syntax around the items we’re passing in, but it has to loop through all of the (properly formatted) properties you give it in order to add them. It’s also worth noting that you wouldn’t want have more than one set of rules for each selector, unless you really know what you’re doing with your css.
It’s also worth noting that this does not provide completely valid html. There is no doctype declaration nor are the html, head, or body tags. You could fix that if you wanted to, but since we are just displaying this stuff locally, in a WinForm app, I think the html gods can forgive us for this one.
Putting them together
In our DisplayManager
class, we also have this method:
private void LoadFeedInBrowser(object sender, EventArgs e) { var html = new HtmlWriter(); var feed = (Feed)((MetroTile)sender).Tag; try { //feed.LoadFeed() <- this works but it is more correct to use the UpdateFeed() method feed.UpdateFeed(); var feedNameTag = html.CreatePairedTag("p", "", feed.Name); html.AddTag(feedNameTag); string[] htmlCss = { "font: 15px Arial" }; html.AddCSSRule("html", htmlCss); string[] wrapperCss = { "display: block", "height:400px", "overflow: auto", "border:1px solid #444", "border-radius:50px", "margin: 5px" }; html.AddCSSRule(".wrapper", wrapperCss); string[] sectionCss = { "background: #333", "color: #EEE" }; html.AddCSSRule(".section", sectionCss); string[] hrCss = { "height: 10px", "background: #999" }; html.AddCSSRule("hr", hrCss); foreach (Item item in feed.Items) { var pubDateTag = html.CreatePairedTag("div", "", item.PubDate); var hrTag = html.CreateSingleTag("hr", ""); var descriptionTag = html.CreatePairedTag("p", "", item.Description); var linkTag = html.CreateLink(item.Link, "", "View on Site"); var wrapperTag = html.CreatePairedTag("div", "wrapper", item.Title + descriptionTag); var fullTag = html.CreatePairedTag("div", "section", pubDateTag + linkTag + wrapperTag + hrTag); html.AddTag(fullTag); } Browser.DocumentText = html.HTML; var time = DateTime.Now; TimeStamp.Text = "Feed last pulled at " + time.ToString("t") + " on " + time.ToString("D"); SelectedTile = (MetroTile)sender; } catch (Exception ex) { AggForm.Alert(ex.Message); } }
This function gets called every time a tile is clicked. We create an instance of our HtmlWriter
class and we also do some casting to get the appropriate feed from the MetroTile
that was clicked (var feed = (Feed)((MetroTile)sender).Tag;
). Then it gets interesting:
- At this point we want the feed to actually load all of the XML data from the RSS link stored in it, so we call it’s
UpdateFeed
method (you could also useLoadFeed
to get the feed content). We have to do a try catch block here because our feed could throw an exception if the URL it contains is invalid. - Once we have the feed content loaded correctly into the feed object, we can start creating HTML tags for all of the properties.
- That’s what the rest of this function does. It’s pretty straight forward except for the
foreach
loop. - In HTML, we can nest elements (tags). In order to nest elements with our
HtmlWriter
class, you have to concatenate the desired tags together and pass them into theCreate
function as one variable. - Finally, we set the
DocumentText
of theWebBrowser
control equal tohtml.HTML
, which contains all of the css and html that we just created, get the current time and update theTimeStamp
MetroLabel
, and set theSelectedTile
property equal the whicheverMetroTile
sent this click event. (We need this so we can have our Update News button call the click event whenever we want to refresh the feed content)
That’s about it
That’s about all for this one. The video will show how to hook the DisplayManager
up to the rest of the form. Once it’s created in the form, you just have to update the tiles anytime you make a change in the configuration (FeedsManager
) and hookup the Update News button. It’s also worth noting that because this isn’t a Web based RSS reader and some RSS feeds contain relative links that don’t include the domain name in them, most of those links won’t work so we have to provide an alternative link that will. That’s what var linkTag = html.CreateLink(item.Link, "", "View on Site");
is for.