I'm working with TwincatAds.Reactive 6.0.190 in .NET 6 WPF Desktop application.
I'm also using MVVM pattern.
My goal is to create a Class that is going to observe for a PLC Variable changes, collect those variables to a dictionary, and later on use those values in the ViewModel.
Here's the method where I'm attaching the notification and action where I'm handling the notification.
public void AttachNotification(IEnumerable<(string key, Type type)> Symbols)
{
_observerValueNotification = Observer.Create<ValueNotification>(val =>
{
// Does handle really start from 2?
var handle = val.Handle;
if (val.UserData is object[] objects)
{
string tag = objects[handle - 2].ToString();
if (!_values.Any(x => x.Key == tag))
_values.Add(new SymbolModel { Key = tag, Value = val.Value });
else
{
var symbol = _values.First(x => x.Key == tag);
symbol.Value = val.Value;
}
}
ValuesChanged?.Invoke(_values);
});
if (_plcWrapper.AdsClient != null)
{
// Get Symbols from SymbolLoader
List<AnySymbolSpecifier> list = new();
List<string> userData = new();
foreach (var (key, type) in Symbols)
{
list.Add(new AnySymbolSpecifier(key, new AnyTypeSpecifier(type)));
userData.Add(key);
}
_subscription2 = _plcWrapper.AdsClient.WhenNotificationEx(list, NotificationSettings.ImmediatelyOnChange, userData.ToArray())
.Subscribe(_observerValueNotification);
}
}
I'm using ValueNotification simply because, I'd like to use this pattern also for complex PLC Variables like Structs.
As You can see, in the WhenNotificationEx method I'm using UserData[] to provide some sort of identification of what Variable has changed when handling the change.
My idea was to use Handle property from ValueNotification as an indexer in UserData[] to identify what variable I'm dealing with, but for some reason Handle starts from 2.
My question is, is it expected behaviour, does the Handle value really always start from 2?
I've decided that relying on the Handle being index in the UserData array is quite unpredictable as Handle is being created by the Twincat Ads server.
Solved the issue by creating own extension method to the WhenNotificationEx. Turned out IDisposableHandleBag has exactly what I was looking for, which is SourceResultHandles property, where AnySymbolSpecifier and ResultHandle are both stored!
Here's created extension method
public static Dictionary<string, uint> Handles { get; private set; } = new();
public static IObservable<ValueNotification> WhenNotificationWithHandle(this IAdsConnection connection, IList<AnySymbolSpecifier> symbols, NotificationSettings settings)
{
IAdsConnection connection2 = connection;
IList<AnySymbolSpecifier> symbols2 = symbols;
NotificationSettings settings2 = settings;
if (connection2 == null)
{
throw new ArgumentNullException("connection");
}
if (symbols2 == null)
{
throw new ArgumentNullException("symbols");
}
if (symbols2.Count == 0)
{
throw new ArgumentOutOfRangeException("symbols", "Symbol list is empty!");
}
IDisposableHandleBag<AnySymbolSpecifier> bag = null;
EventLoopScheduler scheduler = new EventLoopScheduler();
IObservable<int> whenSymbolChangeObserver = connection2.WhenSymbolVersionChanges(scheduler);
IDisposable whenSymbolChanges = null;
Action<EventHandler<AdsNotificationExEventArgs>> addHandler = delegate (EventHandler<AdsNotificationExEventArgs> h)
{
connection2.AdsNotificationEx += h;
bag = ((IAdsHandleCacheProvider)connection2).CreateNotificationExHandleBag(symbols2, relaxSubErrors: false, settings2, null);
bag.CreateHandles();
// Collect Handles
Handles.Clear();
foreach (var item in bag.SourceResultHandles)
Handles.Add(item.source.InstancePath, item.result.Handle);
whenSymbolChanges = whenSymbolChangeObserver.Subscribe((Action<int>)delegate
{
bag.CreateHandles();
Handles.Clear();
foreach (var item in bag.SourceResultHandles)
Handles.Add(item.source.InstancePath, item.result.Handle);
}, (Action<Exception>)delegate
{
TcTraceSource traceAds = AdsModule.TraceAds;
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(101, 1);
defaultInterpolatedStringHandler.AppendLiteral("The AdsServer '");
defaultInterpolatedStringHandler.AppendFormatted(connection2.Address);
defaultInterpolatedStringHandler.AppendLiteral("' doesn't support SymbolVersionChanged Notifications! Handle recreation is not active!");
traceAds.TraceInformation(defaultInterpolatedStringHandler.ToStringAndClear());
});
};
Action<EventHandler<AdsNotificationExEventArgs>> removeHandler = delegate (EventHandler<AdsNotificationExEventArgs> h)
{
if (whenSymbolChanges != null)
{
whenSymbolChanges.Dispose();
}
scheduler.Dispose();
if (bag != null)
{
bag.Dispose();
bag = null;
Handles.Clear();
}
connection2.AdsNotificationEx -= h;
};
return from ev in Observable.FromEventPattern<EventHandler<AdsNotificationExEventArgs>, AdsNotificationExEventArgs>(addHandler, removeHandler)
where bag.Contains(ev.EventArgs.Handle)
select new ValueNotification(ev.EventArgs, ev.EventArgs.Value);
}
Related
I am looking for tips to improve the readability of my code (which is written in C#). My main concern is usage of consecutive if statements and how should I replace them.
My code:
private Dictionary<Guid, CommerceMediaFileAssociation<T>> GetMediaToContentsAssociations<T>(ref bool stopSignaled, CatalogContentBase catalog, CultureInfo culture, Action<string> onStatusChanged = null)
where T : MediaData
{
IEnumerable<ContentReference> descendentReferences = _contentLoader.GetDescendents(catalog.ContentLink);
var associations = new Dictionary<Guid, CommerceMediaFileAssociation<T>>();
if (descendentReferences.Any())
{
var descendentProducts = _contentLoader.GetItems<BaseProduct>(descendentReferences, culture);
foreach (var product in descendentProducts)
{
if (stopSignaled)
{
onStatusChanged?.Invoke($"Reindexing canceled.");
break;
}
if (product is IAssetContainer assetContainer && (assetContainer?.CommerceMediaCollection?.Any() ?? false))
{
foreach (CommerceMedia media in assetContainer.CommerceMediaCollection)
{
PermanentLinkMap mediaLinkMap = _permanentLinkMapper.Find(media.AssetLink);
if ((mediaLinkMap?.Guid != null) && mediaLinkMap.Guid != Guid.Empty)
{
var productInformation = ProductUtilities.GetProductCategoriesAndPriority(product);
if (associations.TryGetValue(mediaLinkMap.Guid, out CommerceMediaFileAssociation<T> commerceMediaFileAssociations))
{
commerceMediaFileAssociations.Products.Add($"{product.ContentGuid.ToString()}||{product.MetaTitle}");
if (productInformation.Categories?.Any() ?? false)
{
foreach (string category in productInformation.Categories)
{
commerceMediaFileAssociations.ProductCategories.Add(category);
}
}
associations[mediaLinkMap.Guid] = commerceMediaFileAssociations;
}
else
{
var commerceMediaFileAssociation = new CommerceMediaFileAssociation<T>();
commerceMediaFileAssociation.Products.Add($"{product.ContentGuid.ToString()}||{product.MetaTitle}");
foreach (string category in productInformation.Categories)
{
commerceMediaFileAssociation.ProductCategories.Add(category);
}
associations.Add(mediaLinkMap.Guid, commerceMediaFileAssociation);
}
associations[mediaLinkMap.Guid].Priority = productInformation.Priority;
}
}
}
if (product is RockstarProduct rockstar)
{
var files = rockstar.Rockstar_Product_Product_Documents.FilteredItems.Select(x => x.GetContent() as IContentMedia) ?? new List<IContentMedia>();
foreach (var file in files)
{
PermanentLinkMap mediaLinkMap = _permanentLinkMapper.Find(file.ContentLink);
if ((mediaLinkMap?.Guid != null) && mediaLinkMap.Guid != Guid.Empty)
{
var productInformation = ProductUtilities.GetProductCategoriesAndPriority(product);
if (associations.TryGetValue(mediaLinkMap.Guid, out CommerceMediaFileAssociation<T> commerceMediaFileAssociations))
{
commerceMediaFileAssociations.Products.Add($"{product.ContentGuid.ToString()}||{product.MetaTitle}");
if (productInformation.Categories?.Any() ?? false)
{
foreach (string category in productInformation.Categories)
{
commerceMediaFileAssociations.ProductCategories.Add(category);
}
}
associations[mediaLinkMap.Guid] = commerceMediaFileAssociations;
}
else
{
var commerceMediaFileAssociation = new CommerceMediaFileAssociation<T>();
commerceMediaFileAssociation.Products.Add($"{product.ContentGuid.ToString()}||{product.MetaTitle}");
foreach (string category in productInformation.Categories)
{
commerceMediaFileAssociation.ProductCategories.Add(category);
}
associations.Add(mediaLinkMap.Guid, commerceMediaFileAssociation);
}
associations[mediaLinkMap.Guid].Priority = productInformation.Priority;
}
}
}
}
}
return associations;
}
How should I change it in order to make it clean, understandable and maintainable? Should I use guard clauses in this case?
You have a lot of code duplications. Some code parts are almost equivalent. It is easy to make them equal. This allows you extract them to another method.
Also, you have if-else statements where the else-part has a lot in common with the if-part where basically only the first statement differs (after having made the almost equal statements equal). You can extract these statements and execute them after the if-else. Then if-part becomes empty. You can invert the if-else and then drop the else part.
There is also no point in testing a collection with Any() before looping through it. If the collection is empty, the loop body will not be executed anyway.
The extracted method:
private void AddCategoriesAndProducts<T>(Dictionary<Guid, CommerceMediaFileAssociation<T>> associations, BaseProduct product, string link) where T : MediaData
{
PermanentLinkMap mediaLinkMap = _permanentLinkMapper.Find(link);
if (mediaLinkMap?.Guid != null && mediaLinkMap.Guid != Guid.Empty) {
var productInformation = ProductUtilities.GetProductCategoriesAndPriority(product);
if (!associations.TryGetValue(mediaLinkMap.Guid, out CommerceMediaFileAssociation<T> commerceMediaFileAssociations)) {
commerceMediaFileAssociations = new CommerceMediaFileAssociation<T>();
}
commerceMediaFileAssociations.Products.Add($"{product.ContentGuid}||{product.MetaTitle}");
if (productInformation.Categories != null) {
foreach (string category in productInformation.Categories) {
commerceMediaFileAssociations.ProductCategories.Add(category);
}
}
associations[mediaLinkMap.Guid] = commerceMediaFileAssociations;
associations[mediaLinkMap.Guid].Priority = productInformation.Priority;
}
}
The simplified method:
private Dictionary<Guid, CommerceMediaFileAssociation<T>> GetMediaToContentsAssociations<T>(ref bool stopSignaled, CatalogContentBase catalog, CultureInfo culture, Action<string> onStatusChanged = null)
where T : MediaData
{
IEnumerable<ContentReference> descendentReferences = _contentLoader.GetDescendents(catalog.ContentLink);
var associations = new Dictionary<Guid, CommerceMediaFileAssociation<T>>();
if (descendentReferences.Any()) {
var descendentProducts = _contentLoader.GetItems<BaseProduct>(descendentReferences, culture);
foreach (var product in descendentProducts) {
if (stopSignaled) {
onStatusChanged?.Invoke($"Reindexing canceled.");
break;
}
if (product is IAssetContainer assetContainer && assetContainer.CommerceMediaCollection != null) {
foreach (CommerceMedia media in assetContainer.CommerceMediaCollection) {
AddCategoriesAndProducts(associations, product, media.AssetLink);
}
}
if (product is RockstarProduct rockstar) {
var files = rockstar.Rockstar_Product_Product_Documents.FilteredItems.Select(x => x.GetContent() as IContentMedia) ?? new List<IContentMedia>();
foreach (var file in files) {
AddCategoriesAndProducts(associations, product, file.ContentLink);
}
}
}
}
return associations;
}
These methods are still relatively complex and could be split into even smaller methods. Methods like AddAssetProducts and AddRockStartProducts.
So I'm pulling in a list of items and for each item I'm creating an instance of an object to run a task on that item. All the objects are the same, they updated based off of a received message every three seconds. This update does not all occur at once though, sometimes it takes 3.1 seconds, etc. This is data I need to serialize in XML once it all exists so I'm looking for a way to see when its all done.
I've explored tasks in .net 4.6 but that initiates a task and it reports complete and then to run again the task class would initiate it again but in my case that won't work because each instance stays alive and initiates itself when a new message comes in.
What is the best way to have it report it reached the last line of code and then look at a list of these instances and say when all of them show as complete then run task to serialize?
I've included code below of the instance that is running.
private void OnMessageReceived(object sender, MessageReceivedEventArgs e)
{
var eventArgs = new CallDataReceivedEventArgs();
this.OnCallDataReceived(eventArgs);
try
{
List<Tuple<String, TimeSpan>> availInItems = new List<Tuple<string, TimeSpan>>();
List<Tuple<string, int, TimeSpan, string, string, string>> agentlist = new List<Tuple<string, int, TimeSpan, string, string, string>>();
if (e == null)
{
return;
}
List<TimeSpan> listOfTimeSpans = new List<TimeSpan>();
if (e.CmsData != null)
{
#region Gathering Agent Information
// Create a list of all timespans for all _agents in a queue using the property AgentTimeInState
foreach (var item in e.CmsData.Agents)
{
//AgentData = new ScoreBoardAgentDataModel(AgentName, AgentExtension, AgentTimeInState, AgentAuxReason, AgentId, AgentAdcState);
_agentData.AgentName = item.AgName;
_agentData.AgentExtension = item.Extension;
_agentData.AgentAuxReason = item.AuxReasonDescription;
_agentData.AgentId = item.LoginId;
_agentData.AgentAcdState = item.WorkModeDirectionDescription;
_agentData.AgentTimeInState = DateTime.Now - item.DateTimeUpdated;
_agentData.TimeSubmitted = DateTime.Now;
agentlist.Add(Tuple.Create(_agentData.AgentName, _agentData.AgentExtension, _agentData.AgentTimeInState, _agentData.AgentId, _agentData.AgentAcdState, _agentData.AgentAuxReason));
if (_agentData.AgentAcdState == "AVAIL")
{
listOfTimeSpans.Add(_agentData.AgentTimeInState);
availInItems.Add(Tuple.Create(_agentData.AgentName, _agentData.AgentTimeInState));
}
availInItems.Sort((t1, t2) => t1.Item2.CompareTo(t2.Item2));
}
var availInAgents =
agentlist
.Where(ag => ag.Item5 == "AVAIL")
.ToList();
availInAgents.Sort((t1, t2) =>
t1.Item3.CompareTo(t2.Item3));
var max3 = availInAgents.Skip(availInAgents.Count - 3);
max3.Reverse();
_agents.AgentsOnBreak = 0;
foreach (var agent in agentlist)
{
if (!string.IsNullOrEmpty(agent.Item6) && agent.Item6.StartsWith("Break"))
{
_agents.AgentsOnBreak++;
}
}
_agents.AgentsOnLunch = 0;
foreach (var agent in agentlist)
{
//If the current agent's aux reason is Lunch
if (!string.IsNullOrEmpty(agent.Item6) && agent.Item6.StartsWith("Lunch"))
{
//add one to agentsonlunch
_agents.AgentsOnLunch++;
}
}
_agents.NextInLine = string.Empty;
foreach (var agent in max3.Reverse())
{
//assign agent to NextInLine and start a new line
_agents.NextInLine += agent.Item1 + Environment.NewLine;
//reverse NextInLine
_agents.NextInLine.Reverse();
}
_agents.TimeSubmitted = DateTime.Now;
#endregion
#region Gathering Skill Information
_skillData.OldestCall = e.CmsData.Skill.OldestCall;
_skillData.AgentsStaffed = e.CmsData.Skill.AgentsStaffed;
_skillData.AgentsAuxed = e.CmsData.Skill.AgentsInAux;
_skillData.AgentsAvailable = e.CmsData.Skill.AgentsAvailable;
_skillData.AgentsOnCalls = e.CmsData.Skill.AgentsOnAcdCall;
_skillData.CallsWaitingInQueue = e.CmsData.Skill.InQueueInRing;
_skillData.Asa = e.CmsData.Skill.AnswerTimePerAcdCall;
_skillData.TimeSubmitted = DateTime.Now;
_skillData.EstimatedHoldTimeLow = e.CmsData.Skill.ExpectedWaitTimeLow;
_skillData.EstimatedHoldTimeMedium = e.CmsData.Skill.ExpectedWaitTimeMedium;
_skillData.EstimatedHoldTimeHigh = e.CmsData.Skill.ExpectedWaitTimeHigh;
#endregion
}
}
catch (Exception ex)
{
_logger.Info(ex.Message, ex);
}
}
With tasks you can start many at the same time and wait for them all to finish like this:
var taskList = new List<Task>();
foreach (var thingToDo in work)
{
taskList.Add(thingToDo.StartTask());
}
Task.WaitAll(taskList.ToArray());
This way you can run everything in parallel and wont get after the last line until everything is done.
Edit following your comment
You can embed your work in a task with this:
public async Task DoWork()
{
var taskList = new List<Task>();
foreach (var thingToDo in work)
{
taskList.Add(thingToDo.StartTask());
}
await Task.WhenAll(taskList.ToArray());
}
I have an add-in for Visual Studio for Java like profile files. A sample file can be found here.
The problem occurs when the \ character is inserted on a specific line. When backwards slash is used, the next line is interpreted as a value (or as a key, or as a key and a value). A simple example is:
1. part1_key\
2. part2_key\
3. part3_key = value
4. key = part1_value\
5. part2_value
The syntax highlighter works when the file is loaded, but when a line is modified, only that line is evaluated and highlighted. So, when the \ is inserted on line 4 for example, the line 4 is highlighted, but the line 5 is not updated. The line 5 is updated only when it's modified (add or remove a character, or a space).
I've created a ITaggerProvider which creates an ITagger whose type is Key, Value or Comment. The ITagger class is as follows:
internal sealed class PropertiesTokenTagger : ITagger<PropertiesTokenTag> {
private readonly Regex keyValuePattern = new Regex(#"(?<!^\s*|\\)([ \t]*[=:][ \t]*|[ \t]+)");
private readonly Regex separatorPattern = new Regex(#"^([=:]|[ \t]+)");
private readonly Regex commentPattern = new Regex(#"^\s*[#!]");
private readonly Regex escapedLineEndPattern = new Regex(#"\\$");
public event EventHandler<SnapshotSpanEventArgs> TagsChanged {
add { }
remove { }
}
public IEnumerable<ITagSpan<PropertiesTokenTag>> GetTags(NormalizedSnapshotSpanCollection spans) {
// sadly `spans` gets one line at a time, so previouslyEscapedValue will not get the chance to be used
foreach (var curSpan in spans) {
var containingLine = curSpan.Start.GetContainingLine();
var lineStartLoc = containingLine.Start.Position;
var lineText = containingLine.GetText();
var previousIsNotComment = false;
var previousLine = curSpan.Snapshot.Lines.LastOrDefault(l => l.End <= curSpan.Start);
if (previousLine != null && escapedLineEndPattern.IsMatch(previousLine.GetText())) {
var previousToken = GetTags(new NormalizedSnapshotSpanCollection(previousLine.Extent)).ToList();
if (previousToken.Count > 0) {
var propertiesTokenTypes = previousToken.Last().Tag.Type;
if (propertiesTokenTypes == PropertiesValue) {
var valueSpan = new SnapshotSpan(curSpan.Snapshot, new Span(lineStartLoc, lineText.Length));
yield return new TagSpan<PropertiesTokenTag>(valueSpan, new PropertiesTokenTag(PropertiesValue));
continue;
}
if (propertiesTokenTypes == PropertiesKey && separatorPattern.IsMatch(lineText)) {
var valueSpan = new SnapshotSpan(curSpan.Snapshot, new Span(lineStartLoc, lineText.Length));
yield return new TagSpan<PropertiesTokenTag>(valueSpan, new PropertiesTokenTag(PropertiesValue));
continue;
}
previousIsNotComment = propertiesTokenTypes != PropertiesComment;
}
}
if (commentPattern.IsMatch(lineText) && !previousIsNotComment) {
var commentSpan = new SnapshotSpan(curSpan.Snapshot, new Span(lineStartLoc, lineText.Length));
yield return new TagSpan<PropertiesTokenTag>(commentSpan, new PropertiesTokenTag(PropertiesComment));
continue;
}
if (keyValuePattern.IsMatch(lineText)) {
var splitPosition = keyValuePattern.Split(lineText)[0].Length;
var keySpan = new SnapshotSpan(curSpan.Snapshot, new Span(lineStartLoc, splitPosition));
yield return new TagSpan<PropertiesTokenTag>(keySpan, new PropertiesTokenTag(PropertiesKey));
var valueSpan = new SnapshotSpan(curSpan.Snapshot, new Span(lineStartLoc + splitPosition + 1, lineText.Length - splitPosition - 1));
yield return new TagSpan<PropertiesTokenTag>(valueSpan, new PropertiesTokenTag(PropertiesValue));
} else {
var keySpan = new SnapshotSpan(curSpan.Snapshot, new Span(lineStartLoc, lineText.Length));
yield return new TagSpan<PropertiesTokenTag>(keySpan, new PropertiesTokenTag(PropertiesKey));
}
}
}
From what I see, the problem is the fact that GetTags method is called from an external source, and I cannot call manually this method in order to update other lines. This method return a List of TagSpans which intersects the NormalizedSnapshotSpanCollection parameter, so if I try to return a tag for another line other than current line, that tag won't be processed.
LE: I've done it, the solution was to raise the TagsChanged event from outside GetTags method, and also in aggregator from ITagger.
I'm currently porting an ASP.NET 2.0 application to Apache/Mono (Ubuntu 13.10).
The application uses XSLT to create ASP.NET controls from dynamic content. It does so by using the (infamous) Page.ParseControl(string) method.
Some pages might have >500 dynamic controls. Calling ParseControl on them still used to take only a few milliseconds on IIS/Windows and is only done on initial page-load. For async. post-backs, only a small number of those controls is re-created (like the one that was "clicked") to optimize that stuff.
Now to my problem with Mono:
ParseControl takes between 300-1500 ms for EACH call. Passing those "500 controls" in a loop would probably take forever, so I already optimized it:
Put ALL controls in a wrapper-DIV
Call ParseControl on that
Extract all single elements via C# code
This takes exactly the same time as parsing only 1. The cost of ParseControl seems to be calling it at all I guess. Also it is only that slow, if I pass "new" content to it (so there seems to be some caching already).
ParseControl creates files in /tmp and then starts the ASP.NET-compiler on them as far as I can see. How could I speed this up?
The Mono-sources are full of TODOs refering to that functionallity and it no longer works at all with Ubuntu 14.04 (throws "System.Web.Compilation.CompilationException", see http://mono.1490590.n4.nabble.com/CS1576-after-upgrade-to-Ubuntu-14-04-1-td4663599.html)
Here is my own implementation of ParseControl. It is far from being complete, so don't expect too much.
Results to expect:
Non-server-controls will be put into "LiteralControl"s.
Supported server-controls (runat="server"):
"HtmlControl"s
Controls from "System.Web" assembly
Custom controls defined in "Web.config"
Others (like user controls) should be easy to add (I'm not using them in my project right now).
The result (if it works) will be something similar to what "Page.ParseControl" does, but NOT THE SAME.
Performance:
I use some caching to speed things up, but it will still be ~50% slower on Windows than "Page.ParseControl".
For Mono (tested with 3.12.1) it is actually usable now.
Prerequesites:
Include "HtmlAgilityPack" from http://htmlagilitypack.codeplex.com
Usage:
String html = "<div runat=\"server\" id=\"test123\"></div>";
Control c = null;
try {
// Performance tip: The "doc" object can be cached, if the same HTML needs to be parsed again
var doc = new HtmlDocument();
doc.LoadHtml(html);
c = OwnParseControlEngine.Parse(doc);
}
catch {
c = Page.ParseControl(html); // Note: Will crash with Mono 3.x
}
Code:
/// <summary>
/// Own implementation of "ParseControl". Returns XHTML (default) or HTML.
/// Custom controls from "Web.config" are supported (TODO: User controls and imports on Page are NOT).
/// </summary>
private class OwnParseControlEngine {
public class ParseException : System.Exception {
public ParseException(HtmlNode e)
: base("Unknown ASP.NET server-tag \"" + e.OriginalName + "\".") {
}
}
private static readonly String _systemWebNamespace;
private static readonly String _systemWebAssembly;
private static readonly Dictionary<String, LinkedList<TagPrefixInfo>> _controlsTagPrefixInfos = new Dictionary<String, LinkedList<TagPrefixInfo>>(); // Key is tag-prefix in lowercase
private class Factory {
public delegate Control CreateDel(HtmlNode e);
private readonly CreateDel _del;
public Boolean DropWhiteSpaceLiterals { get; private set; }
public Factory(CreateDel del, Boolean dropWhiteSpaceLiterals = false) {
this._del = del;
this.DropWhiteSpaceLiterals = dropWhiteSpaceLiterals;
}
public Control Create(HtmlNode e) {
return this._del.Invoke(e);
}
}
private static readonly Dictionary<String, Factory> _factories = new Dictionary<String, Factory>(); // Must be locked. Key is tag-name in lowercase.
static OwnParseControlEngine() {
// We cache the results to speed things up. "Panel" is only used to get assembly info.
_systemWebNamespace = typeof(Panel).Namespace;
_systemWebAssembly = typeof(Panel).Assembly.FullName;
var section = (PagesSection)WebConfigurationManager.OpenWebConfiguration("/").GetSection("system.web/pages");
foreach (TagPrefixInfo info in section.Controls) {
LinkedList<TagPrefixInfo> l;
if (!_controlsTagPrefixInfos.TryGetValue(info.TagPrefix, out l)) {
l = new LinkedList<TagPrefixInfo>();
_controlsTagPrefixInfos.Add(info.TagPrefix.ToLower(), l);
}
l.AddLast(info);
}
// Add HTML control types
_factories.Add("span", new Factory((e) => { return new HtmlGenericControl(e.OriginalName); }));
_factories.Add("div", new Factory((e) => { return new HtmlGenericControl(e.OriginalName); }));
_factories.Add("body", new Factory((e) => { return new HtmlGenericControl(e.OriginalName); }));
_factories.Add("font", new Factory((e) => { return new HtmlGenericControl(e.OriginalName); }));
_factories.Add("a", new Factory((e) => { return new HtmlAnchor(); }));
_factories.Add("button", new Factory((e) => { return new HtmlButton(); }));
_factories.Add("form", new Factory((e) => { return new HtmlForm(); }));
_factories.Add("input", new Factory((e) => {
switch (e.Attributes["type"].Value) {
case "button": return new HtmlInputButton();
case "checkbox": return new HtmlInputCheckBox();
case "file": return new HtmlInputFile();
case "hidden": return new HtmlInputHidden();
case "image": return new HtmlInputImage();
case "radio": return new HtmlInputRadioButton();
case "text": return new HtmlInputText();
case "password": return new HtmlInputPassword();
case "reset": return new HtmlInputReset();
case "submit": return new HtmlInputSubmit();
}
throw new ParseException(e);
}));
_factories.Add("select", new Factory((e) => { return new HtmlSelect(); }));
_factories.Add("table", new Factory((e) => { return new HtmlTable(); }, true)); // Adding literals not allowed
_factories.Add("tr", new Factory((e) => { return new HtmlTableRow(); }, true)); // Adding literals not allowed
_factories.Add("td", new Factory((e) => { return new HtmlTableCell(); }));
_factories.Add("textarea", new Factory((e) => { return new HtmlTextArea(); }));
_factories.Add("link", new Factory((e) => { return new HtmlLink(); }));
_factories.Add("meta", new Factory((e) => { return new HtmlMeta(); }));
_factories.Add("title", new Factory((e) => { return new HtmlTitle(); }));
_factories.Add("img", new Factory((e) => { return new HtmlImage(); }));
}
private static void ApplyHtmlControlAttributes(HtmlControl c, HtmlNode e) {
foreach (HtmlAttribute a in e.Attributes) {
if (a.Name == "id")
c.ID = a.Value;
else if (a.Name != "runat")
c.Attributes[a.OriginalName] = HttpUtility.HtmlDecode(a.Value);
}
}
private static void ApplyControlAttributes(Control c, HtmlNode e) {
if (c is WebControl && e.Attributes["style"] != null) {
String style = HttpUtility.HtmlDecode(e.Attributes["style"].Value);
foreach (String s in style.Split(new Char[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
((WebControl)c).Style[s.Substring(0, s.IndexOf(':'))] = s.Substring(s.IndexOf(':') + 1);
}
foreach (PropertyInfo p in c.GetType().GetProperties()) {
if (p.CanRead && p.CanWrite && e.Attributes[p.Name] != null) {
try {
Object v = null;
if (p.PropertyType.IsEnum)
v = Enum.Parse(p.PropertyType, e.Attributes[p.Name].Value);
else if (p.PropertyType == typeof(String))
v = e.Attributes[p.Name].Value;
else if (p.PropertyType == typeof(Boolean))
v = Boolean.Parse(e.Attributes[p.Name].Value);
else if (p.PropertyType == typeof(Int32))
v = Int32.Parse(e.Attributes[p.Name].Value);
else if (p.PropertyType == typeof(Unit))
v = Unit.Parse(e.Attributes[p.Name].Value);
// TODO: More types?
if (v != null)
p.SetValue(c, v, null);
}
catch {
}
}
}
}
private static Control CreateServerControl(HtmlNode e, out Boolean dropWhiteSpaceLiterals) {
Factory cf;
lock (_factories) {
_factories.TryGetValue(e.Name, out cf);
}
if (cf == null) {
Int32 pos = e.Name.IndexOf(':');
if (pos != -1) {
String tagPrefix = e.Name.Substring(0, pos).ToLower();
String name = e.Name.Substring(pos + 1);
Type t = null;
// Try "System.Web" (default assembly)
if (tagPrefix == "asp")
t = Type.GetType(String.Format("{0}.{1}, {2}", _systemWebNamespace, name, _systemWebAssembly), false, true); // "Namespace.ClassName, Assembly"
if (t == null) {
// Try controls specified in "web.config"
LinkedList<TagPrefixInfo> l;
if (_controlsTagPrefixInfos.TryGetValue(tagPrefix, out l)) {
foreach (var info in l) {
// Custom controls
t = Type.GetType(String.Format("{0}.{1}, {2}", info.Namespace, name, info.Assembly), false, true); // "Namespace.ClassName, Assembly"
if (t != null)
break;
// TODO: User controls with tag.TagName, tag.Source
}
}
}
if (t != null) {
cf = new Factory((e2) => { return (Control)Activator.CreateInstance(t); });
lock (_factories) {
_factories[e.Name] = cf; // "Replace" instead of "Add", because another thread might have already added it since the lock above
}
}
}
}
if (cf == null)
throw new ParseException(e);
var c = cf.Create(e);
if (c is HtmlControl)
ApplyHtmlControlAttributes((HtmlControl)c, e);
else
ApplyControlAttributes(c, e);
dropWhiteSpaceLiterals = cf.DropWhiteSpaceLiterals;
return c;
}
private static void ParseChildren(Control parentC, HtmlNode currE, Boolean xhtml = true, Boolean dropWhiteSpaceLiterals = false) {
foreach (HtmlNode childE in currE.ChildNodes) {
Control newC = null, closeTag = null;
Boolean newDropWhiteSpaceLiterals = false;
if (childE.Attributes["runat"] != null && childE.Attributes["runat"].Value.ToLower() == "server") // Server control
newC = CreateServerControl(childE, out newDropWhiteSpaceLiterals);
else { // Literal control
switch (childE.Name) {
case "#text":
if (!dropWhiteSpaceLiterals || childE.InnerText.Trim().Length != 0)
newC = new LiteralControl(childE.InnerText);
break;
default:
String s = String.Format("<{0}", childE.OriginalName);
foreach (HtmlAttribute a in childE.Attributes)
s += String.Format(" {0}=\"{1}\"", a.OriginalName, a.Value);
s += ">";
switch (childE.Name) {
// List of void elements taken from http://www.programmerinterview.com/index.php/html5/void-elements-html5/
case "area": case "base": case "br": case "col": case "command": case "embed": case "hr": case "img": case "input":
case "keygen": case "link": case "meta": case "param": case "source": case "track": case "wbr":
if (xhtml)
s = s.Substring(0, s.Length - 1) + "/>";
newC = new LiteralControl(s);
break;
default:
newC = new PlaceHolder(); // Used as a wrapper to allow child-controls
newC.Controls.Add(new LiteralControl(s));
closeTag = new LiteralControl(String.Format("</{0}>", childE.OriginalName));
break;
}
break;
}
}
if (newC != null) {
parentC.Controls.Add(newC);
ParseChildren(newC, childE, xhtml, newDropWhiteSpaceLiterals);
if (closeTag != null)
newC.Controls.Add(closeTag);
}
}
}
private OwnParseControlEngine() {
}
/// <summary>
/// Parses the given HTML document and returns a Control.
/// Throws "ParseException" on error (TODO: Maybe others too).
/// </summary>
public static Control Parse(HtmlDocument doc) {
var c = new Control();
ParseChildren(c, doc.DocumentNode, false);
return c;
}
}
Is there a way to query an XmlSchema or XmlSchemaSet for a list of available tags/attributes at a certain point in the XML? So say my cursor is between <b> and </b> and my schema only allows for a <c/> element there, can I figure that out using anything built in to C#?
<tagset>
<a></a>
<b><!-- CURSOR IS HERE --></b>
</tagset>
There is a way, but the Xml Schema specification is complex so it will take some effort and a few hundred lines of code.
The GetExpectedParticles method of the .NET XmlSchemaValidator class is the key part to a solution. This uses the XmlSchemaSet, passed as an argument, to return a set of XmlSchemaObject instances.
Before you can call this method you need to build a node path to your cursor location which must include ancestor elements and their preceding siblings and also the preceding siblings at the current nesting level. This node path is used to set the context for the schema validator.
After GetExpectedParticles has been called you need to process the particles. For instance, check if each the expected particle is a member of a substitution group, and check whether the expected particle is a restricted simple type that's an enumeration.
It's probably best to separate out code that fetches expected elements and attributes respectively.
The following incomplete code snippet includes the GetExpectedParticles method call, this only caters for element tag content, not attributes:
public static List<XmlSchemaObject> XsdExpectedElements(XmlSchemaSet schemaSet,
List<NodeDescriptor> nodePath)
{
List<XmlSchemaObject> elementNames = new List<XmlSchemaObject>();
NameTable nt = new NameTable();
XmlNamespaceManager manager = new XmlNamespaceManager(nt);
XmlSchemaValidator validator = new XmlSchemaValidator(nt, schemaSet, manager, XmlSchemaValidationFlags.None);
// event handler sets validationErrorFound local field
validator.ValidationEventHandler += new ValidationEventHandler(validator_ValidationEventHandler);
validator.Initialize();
XmlSchemaInfo xsInfo = new XmlSchemaInfo();
int i = 0;
foreach (nodeDescriptor nameUri in nodePath)
{
validator.ValidateElement(nameUri.LocalName, nameUri.NamespaceUri, xsInfo);
if ((i >= siblingPosition && siblingPosition > -1) || nameUri.Closed)
{
validator.SkipToEndElement(null);
}
else
{
validator.ValidateEndOfAttributes(null);
}
i++;
}
XmlSchemaParticle[] parts = validator.GetExpectedParticles();
if (parts.Length == 0)
{
bool hasElements = true;
bool elementClosed = nodePath[nodePath.Count - 1].Closed;
if (elementClosed) // we're outside the element tags
{
hasElements = true;
}
else if (xsInfo.SchemaType is XmlSchemaSimpleType)
{
hasElements = false;
}
else
{
XmlSchemaComplexType xsCt = xsInfo.SchemaType as XmlSchemaComplexType;
XmlSchemaContentType xsContent = (XmlSchemaContentType)xsCt.ContentType;
if (xsContent == XmlSchemaContentType.TextOnly)
{
hasElements = false;
}
}
if (!hasElements)
{
expectedType = XmlEditor.expectedListType.elementValue;
if (xsInfo.SchemaElement != null)
{
elementNames.Add(xsInfo.SchemaElement);
}
}
return elementNames;
}
foreach (XmlSchemaObject xso in parts)
{
if (xso is XmlSchemaElement)
{
XmlSchemaElement xse = (XmlSchemaElement)xso;
if (subGroupList.ContainsKey(xse.QualifiedName))
{
List<XmlSchemaElement> xses = subGroupList[xse.QualifiedName];
foreach (XmlSchemaElement xseInstance in xses)
{
elementNames.Add(xseInstance);
}
}
else
{
elementNames.Add(xse);
}
}
else if (xso is XmlSchemaAny)
{
XmlSchemaAny xsa = (XmlSchemaAny)xso;
foreach (XmlSchema xs in schemaSet.Schemas())
{
if (xs.TargetNamespace == xsa.Namespace)
{
foreach (XmlSchemaElement xseAny in xs.Elements)
{
elementNames.Add(xseAny);
}
}
}
}
}
}
The following (incomplete) code snippet shows how to get expected enumerated values from a particle:
private List<string> ExpectedEnumValues(XmlSchemaObject xsso)
{
XmlSchemaSimpleType xst = null;
XmlSchemaComplexType xsCt = null;
List<string> values = new List<string>();
if (xsso == null)
{
return values;
}
if (xsso is XmlSchemaAttribute)
{
XmlSchemaAttribute xsa = (XmlSchemaAttribute)xsso;
xst = xsa.AttributeSchemaType;
}
else
{
XmlSchemaElement xse = (XmlSchemaElement)xsso;
XmlSchemaType gxst = xse.ElementSchemaType;
if (gxst is XmlSchemaSimpleType)
{
xst = (XmlSchemaSimpleType)gxst;
}
else if (gxst is XmlSchemaComplexType)
{
xsCt = (XmlSchemaComplexType)gxst;
}
else
{
return values;
}
}
if(xst != null)
{
if (xst.TypeCode == XmlTypeCode.Boolean)
{
values.Add("true");
values.Add("false");
}
else
{
ProcessXmlSimpleType(xst, values);
}
}
else if (xsCt != null)
{
XmlSchemaContentType xsContent = (XmlSchemaContentType) xsCt.ContentType;
XmlSchemaContentModel xsModel = (XmlSchemaContentModel)xsCt.ContentModel;
if (xsModel is XmlSchemaSimpleContent)
{
XmlSchemaSimpleContent xsSC = (XmlSchemaSimpleContent)xsModel;
XmlSchemaContent xsRE = xsSC.Content;
if (xsRE != null)
{
if (xsRE is XmlSchemaSimpleContentRestriction)
{
XmlSchemaSimpleContentRestriction xsCCR = (XmlSchemaSimpleContentRestriction)xsRE;
foreach (XmlSchemaObject xso in xsCCR.Facets)
{
if (xso is XmlSchemaEnumerationFacet)
{
XmlSchemaEnumerationFacet xsef = (XmlSchemaEnumerationFacet)xso;
values.Add(xsef.Value);
}
}
}
}
}
else
{
XmlSchemaComplexContent xsCC = (XmlSchemaComplexContent)xsModel;
XmlSchemaContent xsRE = xsCC.Content;
if (xsRE != null)
{
if (xsRE is XmlSchemaComplexContentRestriction)
{
XmlSchemaComplexContentRestriction xsR = (XmlSchemaComplexContentRestriction)xsRE;
}
else if (xsRE is XmlSchemaComplexContentExtension)
{
XmlSchemaComplexContentExtension xsE = (XmlSchemaComplexContentExtension)xsRE;
}
}
}
}
return values;
}
And to process a simple type:
private static void ProcessXmlSimpleType(XmlSchemaSimpleType xst, List<string> values)
{
if (xst == null)
{
return;
}
XmlSchemaSimpleTypeContent xsstc = xst.Content;
if (xsstc is XmlSchemaSimpleTypeRestriction)
{
XmlSchemaSimpleTypeRestriction xsr = (XmlSchemaSimpleTypeRestriction)xsstc;
XmlSchemaObjectCollection xsoc = xsr.Facets;
XmlSchemaSimpleType bastTypeOfRestiction = xsr.BaseType;
foreach (XmlSchemaObject xso in xsoc)
{
if (xso is XmlSchemaEnumerationFacet)
{
XmlSchemaEnumerationFacet xsef = (XmlSchemaEnumerationFacet)xso;
values.Add(xsef.Value);
}
}
}
else if (xsstc is XmlSchemaSimpleTypeList)
{
XmlSchemaSimpleTypeList xsstL = (XmlSchemaSimpleTypeList)xsstc;
XmlSchemaSimpleType xstL = xsstL.BaseItemType;
ProcessXmlSimpleType(xstL, values); // recursive
}
else if (xsstc is XmlSchemaSimpleTypeUnion)
{
XmlSchemaSimpleTypeUnion xstU = (XmlSchemaSimpleTypeUnion)xsstc;
XmlSchemaSimpleType[] xsstArray = xstU.BaseMemberTypes;
foreach (XmlSchemaSimpleType xsstA in xsstArray)
{
ProcessXmlSimpleType(xsstA, values); // recursive
}
}
}
The above code snippets probably address 20% of what's needed, but hopefully give you some idea of what you will be dealing with. .NET provides a very powerful set of classes for analysing the Schema Object Model, but you will need detailed knowledge of the XML Schema specification to get usable results.
XML editors should still provide auto-completion help when the XML is not valid, this adds an extra dimension to the problem because there may be ambiguities if there's limited validation context and the schema design is more 'russian-doll' than 'salami sliced'.
Summary
Getting a list of expected XML schema particles for a given context within an XML instance using .NET is possible but relatively complex. In view of this, it would be worthwhile to first check if libraries from existing .NET XML editors provide the functionality you need.
For a working implementation under LGPL have a look at SharpDevelops XmlEditor part.
You get the code completion for xml in one dll, namely the XmlEditor.dll in the AddIns/DisplayBindings directory.