I'm pulling feeds for my RSS project and I am running into a problem of not knowing how to allow the user to load more items into the collection. At the current moment, everything loads at once. While this is in some cases all right, I would like the user to be able to choose how things get loaded in case they have a slow mobile connection.
This is borrowed code and thus it only adds to my confusion.
Where could i be able to inject code into this sample to allow a dynamic loading of items, say, 30 at a time?
Rss Class:
namespace MyRSSService
{
public class RssService
{
/// Gets the RSS items.
/// <param name="rssFeed">The RSS feed.</param>
/// <param name="onGetRssItemsCompleted">The on get RSS items completed.</param>
/// <param name="onError">The on error.</param>
public static void GetRssItems(string rssFeed, Action<IList<RssItem>> onGetRssItemsCompleted = null, Action<Exception> onError = null, Action onFinally = null)
{
WebClient webClient = new WebClient();
// register on download complete event
webClient.OpenReadCompleted += delegate(object sender, OpenReadCompletedEventArgs e)
{
try
{
// report error
if (e.Error != null)
{
if (onError != null)
{
onError(e.Error);
}
return;
}
// convert rss result to model
IList<RssItem> rssItems = new List<RssItem>();
Stream stream = e.Result;
XmlReader response = XmlReader.Create(stream);
{
SyndicationFeed feeds = SyndicationFeed.Load(response);
foreach (SyndicationItem f in feeds.Items)
{
RssItem rssItem = new RssItem(f.Title.Text, f.Summary.Text, f.PublishDate.ToString(), f.Links[0].Uri.AbsoluteUri);
rssItems.Add(rssItem);
}
}
// notify completed callback
if (onGetRssItemsCompleted != null)
{
onGetRssItemsCompleted(rssItems);
}
}
finally
{
// notify finally callback
if (onFinally != null)
{
onFinally();
}
}
};
webClient.OpenReadAsync(new Uri(rssFeed));
}
}
}
items setting class:
namespace MyRSSService
{
public class RssItem
{
/// <summary>
/// Initializes a new instance of the <see cref="RssItem"/> class.
/// </summary>
/// <param name="title">The title.</param>
/// <param name="summary">The summary.</param>
/// <param name="publishedDate">The published date.</param>
/// <param name="url">The URL.</param>
public RssItem(string title, string summary, string publishedDate, string url)
{
Title = title;
Summary = summary;
PublishedDate = publishedDate;
Url = url;
// Get plain text from html
PlainSummary = HttpUtility.HtmlDecode(Regex.Replace(summary, "<[^>]+?>", ""));
}
public string Title { get; set; }
public string Summary { get; set; }
public string PublishedDate { get; set; }
public string Url { get; set; }
public string PlainSummary { get; set; }
}
}
the binding C# to the page to display the feeds
public partial class FeedPage : PhoneApplicationPage
{
private const string WindowsPhoneBlogPosts = "http://feeds.bbci.co.uk/news/rss.xml";
public FeedPage()
{
InitializeComponent();
RssService.GetRssItems(WindowsPhoneBlogPosts, (items) => { listbox.ItemsSource = items; }, (exception) => { MessageBox.Show(exception.Message); }, null);
}
}
Unless the server where the feed is hosted provides an API to limit the number of returned items (for example, this practice is used for the Xbox Marketplace), you will be downloading the entire feed, even if you decide to only show a part of it.
Related
I ran into an issue recently when in an existing Console Application project we're forced to remove a reference to WSE 3.0. When I tried to remove it from references (because I could not find where it's used) it turned out there is only one place and it's using the SoapEnvelope class.
A little bit about the application: it's a console application that connects to an Exchange Server and listens to incoming emails, around 2-3k emails daily.
The SoapEnvelope class is used to read and parse the email body:
/// <summary>
/// Reads content of received HTTP request.
/// </summary>
/// <param name="client">The specified client.</param>
/// <returns>The watermark string if the read is successful; otherwise the last watermark.</returns>
public string Read(TcpClient client)
{
try
{
_myReadBuffer = new byte[client.ReceiveBufferSize];
using (var networkStream = client.GetStream())
{
var httpRequest = new MailboxHttpRequest(networkStream);
if (httpRequest.HasBody)
{
var httpResponse = new MailboxHttpResponse(_mailbox);
httpResponse.ParseBody(httpRequest.Body);
httpResponse.Send(networkStream, httpRequest.Body);
}
return httpRequest.Watermark;
}
}
catch (Exception ex)
{
Logger.WriteErrorLine("[EventsCollector] Error calling Read in MailboxNotificationManager", ex);
return _mailbox.LastWatermark;
}
finally
{
_myReadBuffer = null;
}
}
And the ParseBody methods looks like this:
/// <summary>
/// Parses the XML body content of receive notification and initialized email processing, if new message was received.
/// </summary>
/// <param name="body">XML content of received notification.</param>
public void ParseBody(string body)
{
var soapEnvelope = new SoapEnvelope() { InnerXml = body };
var serializer = new XmlSerializer(typeof(SendNotificationResponseType));
using (var reader = new XmlNodeReader(soapEnvelope.Body.FirstChild))
{
var notificationResponse = (SendNotificationResponseType)serializer.Deserialize(reader);
if (notificationResponse.ResponseMessages != null) // Process notification, if request contains response message
{
_result = processNotification(notificationResponse);
}
}
}
As you can see, it creates a SoapEnvelope class to access the body`s first child.
MailboxHttpRequest class
private class MailboxHttpRequest
{
private readonly Regex _httpRequestBodyLengthPattern = new Regex(#"Content-Length: (?<BodyLength>\d*)", RegexOptions.Compiled);
private readonly Regex _httpRequestBodyPattern = new Regex(#"<\?xml .*", RegexOptions.Compiled);
private readonly Regex _httpRequestWatermarkPattern = new Regex(#"<t:Watermark>(?<Watermark>.*?)<\/t:Watermark>", RegexOptions.Compiled);
private const int _bufferSize = 8192;
public bool HasBody
{
get { return !String.IsNullOrEmpty(Body); }
}
public string Body { get; private set; }
public string Watermark { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="MailboxHttpRequest"/> class and reads incoming messages
/// </summary>
/// <param name="networkStream">The network stream.</param>
public MailboxHttpRequest(NetworkStream networkStream)
{
var completeRequest = new StringBuilder();
var readBuffer = new byte[_bufferSize];
do // Read incoming message that might consist of many parts
{
var newDataLength = networkStream.Read(readBuffer, 0, readBuffer.Length);
completeRequest.Append(Encoding.ASCII.GetString(readBuffer, 0, newDataLength));
}
while (networkStream.DataAvailable || !requestWasFullyReceived(completeRequest.ToString()));
Body = _httpRequestBodyPattern.Match(completeRequest.ToString())
.Value;
Watermark = _httpRequestWatermarkPattern.Match(Body).Groups["Watermark"].Value;
}
}
The downside is that I`m not able test this part of code since I cant listen to same mailbox so I cant check what and how does the passed string look like.
If anybody has any suggestion on how to replace the SoapEnvelope class, would be greatly appreciate it.
Cosmin
I'm developing API via c# that will send notification to specific user ( android user) , then when user open the notification I want to redirect him to specific activity.
So I needed to send data along with notification message. I've tested it using Firebase Console and it's working fine , The notification is received and my launcher activity receive the extra from data has been sent
I've also tested it from my backend and the notification is received except that my launcher intent doesn't receive any extra.
I've been struggling for hours now , Any idea would help !
this is my code from c#
public String getNotification ()
{
string serverKey = "xxxx";
var result = "-1";
try
{
var webAddr = "https://fcm.googleapis.com/fcm/send";
var regID = "xxxx";
var httpWebRequest = (HttpWebRequest)WebRequest.Create(webAddr);
httpWebRequest.ContentType = "application/json";
httpWebRequest.Headers.Add("Authorization:key=" + serverKey);
httpWebRequest.Method = "POST";
using (var streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()))
{
string json = "{\"to\": \"" + regID +
"\",\"notification\": {\"title\": \"Testing\",\"body\": \"Hi Testing\"}" +
"," + "\"data:\"" + "{\"mymsg\":" + "\"h\" }}";
streamWriter.Write(json);
streamWriter.Flush();
}
var httpResponse = (HttpWebResponse)httpWebRequest.GetResponse();
using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
{
result = streamReader.ReadToEnd();
}
return result;
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
return "Can't Send";
}
}
}
And this is my launcher activity :
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("test" , "in main");
if (getIntent().getStringExtra("mymsg") != null) {
Log.d("test" , "has extra");
Intent intent = new Intent(this, Main2Activity.class);
startActivity(intent);
finish();
} else {
Log.d("test" , "no extra");
}
It looks like you have the wrong JSON:
," + "\"data:\"" + "{\"mymsg\":" + "\"h\" }} will be:
"data:" {
"mymsg":"h"
}
just correct your JSON. But I recommend using c# classes and serialization. Look at this simple example:
var payload = new {
to = "XXXX",
notification = new
{
body = "Test",
title = "Test"
},
data = new {
mymsg = "h"
}
}
// Using Newtonsoft.Json
string postbody = JsonConvert.SerializeObject(payload).ToString();
But its just example. You should create classes instead of anonym objects and using JsonProperty or another way to serialize the object. Something like that:
/// <summary>
/// Data for sending push-messages.
/// </summary>
public class PushData
{
/// <summary>
/// [IOS] Displaying message
/// </summary>
[JsonProperty("alert")]
public Alert Alert { get; set; }
/// <summary>
/// [IOS] badge value (can accept string representaion of number or "Increment")
/// </summary>
[JsonProperty("badge")]
public Int32? Badge { get; set; }
/// <summary>
/// [IOS] The name of sound to play
/// </summary>
[JsonProperty("sound")]
public String Sound { get; set; }
/// <summary>
/// [IOS>=7] Content to download in background
/// </summary>
/// <remarks>
/// Set 1 for silent mode
/// </remarks>
[JsonProperty("content-available")]
public Int32? ContentAvailable { get; set; }
/// <summary>
/// [IOS>=8] Category of interactive push with additional actions
/// </summary>
[JsonProperty("category")]
public String Category { get; set; }
/// <summary>
/// [Android] Used for collapsing some messages with same collapse_key
/// </summary>
[JsonProperty(PropertyName = "collapse_key")]
public String CollapseKey { get; set; }
/// <summary>
/// [Android] This parameter specifies how long (in seconds) the message should be kept in GCM storage if the device is offline.
/// The maximum time to live supported is 4 weeks, and the default value is 4 weeks.
/// </summary>
/// <value>
/// Time_to_live value of 0 means messages that can't be delivered immediately will be discarded
/// </value>
[JsonProperty("time_to_live")]
public Int32 TimeToLive { get; set; }
/// <summary>
/// [Android] Uri of activity to open when push activated by user
/// </summary>
[JsonProperty("url")]
public String Url { get; set; }
/// <summary>
/// Payload for push
/// </summary>
[JsonProperty("data")]
public Payload Payload { get; set; }
}
with message builder which serialize your message body to correct json string.
Consider the following set of classes. There are two things I would like to achieve.
Get the string representation of the path of the current property. For example totalAsset.BuildingAsset.HistoricalBuildingAsset.Path should return "TotalAsset.BuildingAsset.HistoricalBuildingAsset"
Given a path "TotalAsset.BuildingAsset.HistoricalBuildingAsset" and a value "100", I want to use the path to retrieve the property and change its value.
Code Example:
public abstract class Field
{
private string _path = string.Empty;
public double Value {get;set;}
public string Path
{
get
{
//Code probably goes here
throw new NotImplementedException();
}
protected set { _path = value; }
}
}
public sealed class TotalAsset : Field
{
public TotalAsset(BuildingAsset buildingAsset)
{
Path = "TotalAsset";
BuildingAsset = buildingAsset;
}
public BuildingAsset BuildingAsset { get; private set; }
}
public sealed class BuildingAsset : Field
{
public HistoricalBuildingAsset HistoricalBuildingAsset { get; private set; }
public BuildingAsset(HistoricalBuildingAsset historicalBuildingAsset)
{
Path = "BuildingAsset";
this.HistoricalBuildingAsset = historicalBuildingAsset;
}
}
public sealed class HistoricalBuildingAsset : Field
{
public HistoricalBuildingAsset()
{
Path = "HistoricalBuildingAsset";
}
}
[TestClass]
public class TestPath
{
[TestMethod]
public void MethodTestPath()
{
var historicalBuildingAsset = new HistoricalBuildingAsset();
var buildingAsset = new BuildingAsset(historicalBuildingAsset);
var totalAsset = new TotalAsset(buildingAsset);
Assert.AreEqual("TotalAsset.BuildingAsset.HistoricalBuildingAsset", totalAsset.BuildingAsset.HistoricalBuildingAsset.Path);
}
}
Wouldn't this be easily solved using polymorphism?
Based on your question, it seems like your Path property has an inmutable value, thus you should be able to solve your issue like the following code:
public class A
{
public virtual string Path
{
get { return "A"; }
}
}
public class B : A
{
public override string Path
{
get { return base.Path + ".B"; }
}
}
public class C : B
{
public override string Path
{
get { return base.Path + ".C"; }
}
}
A a = new A();
Console.WriteLine(a.Path); // Prints "A"
B b = new B();
Console.WriteLine(b.Path); // Prints "A.B"
C c = new C();
Console.WriteLine(c.Path); // Prints "A.B.C"
Update v1.1: Recursive approach (now includes getting a property value and setting a property value by a given object path)
Because you want to leave your model as is and go with the composition way, this is the piece of "magic" to dynamically get the whole path. Note that I've required a new FullPath property in order to avoid an infinite loop during path calculation (you can also try it in a DotNetFiddle):
using System;
using System.Linq;
using System.Reflection;
public abstract class Field
{
public double Value
{
get;
set;
}
public string Path
{
get;
protected set;
}
public string FullPath
{
get
{
return BuildPath(this);
}
}
/// <summary>
/// Recursively-builds a dot-separated full path of associated fields
/// </summary>
/// <param name="field">Optional, it's a reference to current associated field </param>
/// <param name="path">Optional, provided when this method enters to the first associated </param>
/// <returns>The whole dot-separated full path of associations to Field</returns>
private string BuildPath(Field field, string path = "")
{
// Top-level path won't start with dot
if (path != string.Empty)
{
path += '.';
}
path += field.Path;
// This will look for a property which is of type Field
PropertyInfo fieldProperty = field.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
.SingleOrDefault(prop => prop.PropertyType.IsSubclassOf(typeof(Field)));
// If current field has a property of type Field...
if (fieldProperty != null)
{
// ...we'll get its value and we'll start a recursion to find the next Field.Path
path = BuildPath((Field)fieldProperty.GetValue(field, null), path);
}
return path;
}
/// <summary>
/// Recursively sets a value to an associated field property
/// </summary>
/// <param name="path">The whole path to the property</param>
/// <param name="value">The value to set</param>
/// <param name="associatedField">Optional, it's a reference to current associated field</param>
public void SetByPath(string path, object value, Field associatedField = null)
{
if (string.IsNullOrEmpty(path.Trim()))
{
throw new ArgumentException("Path cannot be null or empty");
}
string[] pathParts = path.Split('.');
if (associatedField == null)
{
associatedField = this;
}
// This will look for a property which is of type Field
PropertyInfo property = associatedField.GetType().GetProperty(pathParts[0], BindingFlags.Public | BindingFlags.Instance);
if (property == null)
{
throw new ArgumentException("A property in the path wasn't found", "path");
}
object propertyValue = property.GetValue(associatedField, null);
// If property value isn't a Field, then it's the last part in the path
// and it's the property to set
if (!propertyValue.GetType().IsSubclassOf(typeof(Field)))
{
property.SetValue(associatedField, value);
}
else
{
// ... otherwise, we navigate to the next associated field, removing the first
// part in the path, so the next call will look for the next property...
SetByPath(string.Join(".", pathParts.Skip(1)), value, (Field)propertyValue);
}
}
/// <summary>
/// Recursively gets a value from an associated field property
/// </summary>
/// <param name="path">The whole path to the property</param>
/// <param name="associatedField">Optional, it's a reference to current associated field</param>
/// <typeparam name="T">The type of the property from which the value is going to be obtained</typeparam>
public T GetByPath<T>(string path, Field associatedField = null)
{
if (string.IsNullOrEmpty(path.Trim()))
{
throw new ArgumentException("Path cannot be null or empty");
}
string[] pathParts = path.Split('.');
if (associatedField == null)
{
associatedField = this;
}
// This will look for a property which is of type Field
PropertyInfo property = associatedField.GetType().GetProperty(pathParts[0], BindingFlags.Public | BindingFlags.Instance);
if (property == null)
{
throw new ArgumentException("A property in the path wasn't found", "path");
}
object propertyValue = property.GetValue(associatedField, null);
// If property value isn't a Field, then it's the last part in the path
// and it's the property to set
if (!propertyValue.GetType().IsSubclassOf(typeof(Field)))
{
return (T)property.GetValue(associatedField, null);
}
else
{
// ... otherwise, we navigate to the next associated field, removing the first
// part in the path, so the next call will look for the next property...
return GetByPath<T>(string.Join(".", pathParts.Skip(1)), (Field)propertyValue);
}
}
}
public sealed class TotalAsset : Field
{
public TotalAsset(BuildingAsset buildingAsset)
{
Path = "TotalAsset";
BuildingAsset = buildingAsset;
}
public BuildingAsset BuildingAsset
{
get;
private set;
}
}
public sealed class BuildingAsset : Field
{
public HistoricalBuildingAsset HistoricalBuildingAsset
{
get;
private set;
}
public BuildingAsset(HistoricalBuildingAsset historicalBuildingAsset)
{
Path = "BuildingAsset";
this.HistoricalBuildingAsset = historicalBuildingAsset;
}
}
public sealed class HistoricalBuildingAsset : Field
{
public HistoricalBuildingAsset()
{
Path = "HistoricalBuildingAsset";
}
public int Age
{
get;
set;
}
}
public class Program
{
public static void Main()
{
TotalAsset total = new TotalAsset(new BuildingAsset(new HistoricalBuildingAsset()));
// Prints "TotalAsset.BuildingAsset.HistoricalBuildingAsset"
Console.WriteLine(total.FullPath);
total.SetByPath("BuildingAsset.HistoricalBuildingAsset.Age", 300);
// Prints "300" as expected!
Console.WriteLine(total.GetByPath<int>("BuildingAsset.HistoricalBuildingAsset.Age"));
}
}
You can re-use the existing .net framework Binding pattern and codebase. Your description of what you want to do sounds mightily like MVVM binding to me. The use of Binding in WPF is explained here http://msdn.microsoft.com/en-us/library/ms752347(v=vs.110).aspx.
Using System.Windows.Data.Binding gives you an extensible framework for getting data into and out of object graphs using relative and absolute string paths to nominate the class members and collection indexes.
This is the class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using HtmlAgilityPack;
using System.Net;
namespace GatherLinks
{
/// <summary>
/// A result encapsulating the Url and the HtmlDocument
/// </summary>
class WebPage
{
public Uri Url { get; set; }
/// <summary>
/// Get every WebPage.Internal on a web site (or part of a web site) visiting all internal links just once
/// plus every external page (or other Url) linked to the web site as a WebPage.External
/// </summary>
/// <remarks>
/// Use .OfType WebPage.Internal to get just the internal ones if that's what you want
/// </remarks>
public static IEnumerable<WebPage> GetAllPagesUnder(Uri urlRoot)
{
var queue = new Queue<Uri>();
var allSiteUrls = new HashSet<Uri>();
queue.Enqueue(urlRoot);
allSiteUrls.Add(urlRoot);
while (queue.Count > 0)
{
Uri url = queue.Dequeue();
HttpWebRequest oReq = (HttpWebRequest)WebRequest.Create(url);
oReq.UserAgent = #"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5";
HttpWebResponse resp = (HttpWebResponse)oReq.GetResponse();
WebPage result;
if (resp.ContentType.StartsWith("text/html", StringComparison.InvariantCultureIgnoreCase))
{
HtmlDocument doc = new HtmlDocument();
try
{
var resultStream = resp.GetResponseStream();
doc.Load(resultStream); // The HtmlAgilityPack
result = new Internal() { Url = url, HtmlDocument = doc };
}
catch (System.Net.WebException ex)
{
result = new WebPage.Error() { Url = url, Exception = ex };
}
catch (Exception ex)
{
ex.Data.Add("Url", url); // Annotate the exception with the Url
throw;
}
// Success, hand off the page
yield return new WebPage.Internal() { Url = url, HtmlDocument = doc };
// And and now queue up all the links on this page
foreach (HtmlNode link in doc.DocumentNode.SelectNodes(#"//a[#href]"))
{
HtmlAttribute att = link.Attributes["href"];
if (att == null) continue;
string href = att.Value;
if (href.StartsWith("javascript", StringComparison.InvariantCultureIgnoreCase)) continue; // ignore javascript on buttons using a tags
Uri urlNext = new Uri(href, UriKind.RelativeOrAbsolute);
// Make it absolute if it's relative
if (!urlNext.IsAbsoluteUri)
{
urlNext = new Uri(urlRoot, urlNext);
}
if (!allSiteUrls.Contains(urlNext))
{
allSiteUrls.Add(urlNext); // keep track of every page we've handed off
if (urlRoot.IsBaseOf(urlNext))
{
queue.Enqueue(urlNext);
}
else
{
yield return new WebPage.External() { Url = urlNext };
}
}
}
}
}
}
///// <summary>
///// In the future might provide all the images too??
///// </summary>
//public class Image : WebPage
//{
//}
/// <summary>
/// Error loading page
/// </summary>
public class Error : WebPage
{
public int HttpResult { get; set; }
public Exception Exception { get; set; }
}
/// <summary>
/// External page - not followed
/// </summary>
/// <remarks>
/// No body - go load it yourself
/// </remarks>
public class External : WebPage
{
}
/// <summary>
/// Internal page
/// </summary>
public class Internal : WebPage
{
/// <summary>
/// For internal pages we load the document for you
/// </summary>
public virtual HtmlDocument HtmlDocument { get; internal set; }
}
}
}
It never stop on this line:
public Uri Url { get; set; }
And never stop on any other lines in this class. Only if i remove the line:
public Uri Url { get; set; }
Then it stop on other lines. But i dont get it why it dosent stop on the first line ? How can i fix it ?
I tried ot read about automatic properties but i didnt understand what it is and i didnt want to use it in this class.
Add the breakpoint to where you declare this class.
WebPage wp =new WebPage();
because as Asif said above, it wont stop in a declaration.
Or after you declare the Class set the Url variable
wp.Url="blahblahblah.html";
EDIT: I was unaware of breakpoints not working on auto properties.
change your
public Uri Url{get;set;}
to
private Uri _Url=new Uri();
public Url URL{get{return _Url;}set{_Url = value;}}
what you are doing here is creating a private variable name _Url and accessing it with the property Url
use is
Url="blahblahblah";
same as you are currently using it
Breakpoints are not supported on auto-implemented properties. Try setting it in the first line of the GetAllPagesUnder method.
It's very vague from your question - but I have a hunch that somewhere in your program you're calling:
var results = WebPage.GetAllPagesUnder([some uri]);
And you're expecting breakpoints to be hit when that method is called?
It won't - it's yielding an enumerator. The code doesn't actually do anything until you foreach the enumerable; or perhaps eager load it via ToArray, ToList or something similar.
As for the automatic property, no you can't breakpoint that - but then you shouldn't ever need to - just breakpoint the code that is setting the property instead. If you really want to, stick in a private backing field and implement the property manually. Failing that, give the class a constructor that takes the Uri and sets it, then you breakpoint that instead.
You can set a breakpoint at the Url property like it is described here.
The standard way of setting a breakpoint does not work for auto properties.
I have a string array of some file paths:
path/to/folder/file.xxx
path/to/other/
path/to/file/file.xx
path/file.x
path/
How can I convert this list to a tree structure? So far I have the following:
/// <summary>
/// Enumerates types of filesystem nodes.
/// </summary>
public enum FilesystemNodeType
{
/// <summary>
/// Indicates that the node is a file.
/// </summary>
File,
/// <summary>
/// Indicates that the node is a folder.
/// </summary>
Folder
}
/// <summary>
/// Represents a file or folder node.
/// </summary>
public class FilesystemNode
{
private readonly ICollection<FilesystemNode> _children;
/// <summary>
/// Initializes a new instance of the <see cref="FilesystemNode"/> class.
/// </summary>
public FilesystemNode()
{
_children = new LinkedList<FilesystemNode>();
}
/// <summary>
/// Gets or sets the name of the file or folder.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the full path to the file or folder from the root.
/// </summary>
public string Path { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the node is a file or folder.
/// </summary>
public FilesystemNodeType Type { get; set; }
/// <summary>
/// Gets a list of child nodes of this node. The node type must be a folder to have children.
/// </summary>
public ICollection<FilesystemNode> Children
{
get
{
if (Type == FilesystemNodeType.Folder)
return _children;
throw new InvalidOperationException("File nodes cannot have children");
}
}
}
I'm just a bit at a loss at how to actually split up the paths and all. Any path that ends with a / is a directory, any one that doesn't, is not.
Also, while my input will always contain a path to the folder, how would I account for that situation if it did not?
For example, if I had the input:
path/to/file.c
path/file.c
path/
How would I account for the fact that path/to/ is not in the input?
Here is a solution that generates a nested dictionary of NodeEntry items (you can substitute your file info class as needed):
public class NodeEntry
{
public NodeEntry()
{
this.Children = new NodeEntryCollection();
}
public string Key { get; set; }
public NodeEntryCollection Children { get; set; }
}
public class NodeEntryCollection : Dictionary<string, NodeEntry>
{
public void AddEntry(string sEntry, int wBegIndex)
{
if (wBegIndex < sEntry.Length)
{
string sKey;
int wEndIndex;
wEndIndex = sEntry.IndexOf("/", wBegIndex);
if (wEndIndex == -1)
{
wEndIndex = sEntry.Length;
}
sKey = sEntry.Substring(wBegIndex, wEndIndex - wBegIndex);
if (!string.IsNullOrEmpty(sKey)) {
NodeEntry oItem;
if (this.ContainsKey(sKey)) {
oItem = this[sKey];
} else {
oItem = new NodeEntry();
oItem.Key = sKey;
this.Add(sKey, oItem);
}
// Now add the rest to the new item's children
oItem.Children.AddEntry(sEntry, wEndIndex + 1);
}
}
}
}
To use the above, create a new collection:
NodeEntryCollection cItems = new NodeEntryCollection();
then, for each line in your list:
cItems.AddEntry(sLine, 0);
I have been inspired from competent_tech's answer and replaced the Dictionary<string, NodeEntry> with a "simple" ObservableCollection<NodeEntry> as the "Key" information would be stored twice in this Dictionary: once as key of the Dictionary and once as public property in the NodeEntry class.
So my sample based on the "competent_tech" code looks like the following:
public class NodeEntryObservableCollection : ObservableCollection<NodeEntry>
{
public const string DefaultSeparator = "/";
public NodeEntryObservableCollection(string separator = DefaultSeparator)
{
Separator = separator; // default separator
}
/// <summary>
/// Gets or sets the separator used to split the hierarchy.
/// </summary>
/// <value>
/// The separator.
/// </value>
public string Separator { get; set; }
public void AddEntry(string entry)
{
AddEntry(entry, 0);
}
/// <summary>
/// Parses and adds the entry to the hierarchy, creating any parent entries as required.
/// </summary>
/// <param name="entry">The entry.</param>
/// <param name="startIndex">The start index.</param>
public void AddEntry(string entry, int startIndex)
{
if (startIndex >= entry.Length)
{
return;
}
var endIndex = entry.IndexOf(Separator, startIndex);
if (endIndex == -1)
{
endIndex = entry.Length;
}
var key = entry.Substring(startIndex, endIndex - startIndex);
if (string.IsNullOrEmpty(key))
{
return;
}
NodeEntry item;
item = this.FirstOrDefault(n => n.Key == key);
if (item == null)
{
item = new NodeEntry(Separator) { Key = key };
Add(item);
}
// Now add the rest to the new item's children
item.Children.AddEntry(entry, endIndex + 1);
}
}
public class NodeEntry
{
public string Key { get; set; }
public NodeEntryObservableCollection Children { get; set; }
public NodeEntry(string separator = NodeEntryObservableCollection.DefaultSeparator)
{
Children = new NodeEntryObservableCollection(separator);
}
}
This helps me in binding the data in a TreeView like this:
<TreeView Name="trvMyTreeView">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:NodeEntry}" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Key}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
With a sample code behind like this:
IList<string> pathes = new List<string>
{
"localhost",
"remotehost.levelD.levelDB",
"localhost.level1.level11",
"localhost.level1",
"remotehost.levelD.levelDA",
"localhost.level2.level22",
"remotehost.levelA",
"remotehost",
"remotehost.levelB",
"remotehost.levelD",
"localhost.level2",
"remotehost.levelC"
};
SortedSet<string> sortedPathes = new SortedSet<string>(pathes);
var obsCollection = new NodeEntryObservableCollection(".");
foreach (var p in sortedPathes) { obsCollection.AddEntry(p); }
trvMyTreeView.ItemsSource = obsCollection;
Split each line by the '/' character. If the string array is of length 5, then the first four items should be directories, and you have to test the last for an extension:
string.IsNullOrEmpty(new FileInfo("test").Extension)
If, like in your case, there is always a '/' even for the last directory, then the last item of the split string array is empty.
The rest is just about traversing your tree. When parsing a item, check if the first directory exists in the Children property of your root node. If it not exists, add it, if it does, use this one and go further.