diff --git a/src/DynamoUtilities/CLIWrapper.cs b/src/DynamoUtilities/CLIWrapper.cs new file mode 100644 index 00000000000..5578e233867 --- /dev/null +++ b/src/DynamoUtilities/CLIWrapper.cs @@ -0,0 +1,175 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; + +namespace Dynamo.Utilities +{ + /// + /// Base class for Dynamo CLI wrappers + /// + internal abstract class CLIWrapper : IDisposable + { + protected const string endOfDataToken = @"<<<<>>>>"; + protected const string startofDataToken = @"<<<<>>>>"; + protected readonly Process process = new Process(); + protected bool started; + internal event Action MessageLogged; + + public virtual void Dispose() + { + process.ErrorDataReceived -= Process_ErrorDataReceived; + KillProcess(); + } + + /// + /// Start the process. + /// + /// relative path to the exe to start. + /// argument string to pass to process. + protected virtual void StartProcess(string relativeEXEPath, string argString) + { + ProcessStartInfo startInfo = new ProcessStartInfo + { + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardInput = true, + RedirectStandardError = true, + + UseShellExecute = false, + Arguments = argString, + FileName = GetToolPath(relativeEXEPath) + }; + + process.StartInfo = startInfo; + try + { + process.Start(); + started = true; + //the only purpose here is to avoid deadlocks when std err gets filled up 4kb + //in long running processes. + process.ErrorDataReceived += Process_ErrorDataReceived; + process.BeginErrorReadLine(); + + } + catch (Win32Exception) + { + // Do nothing + } + } + + private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e) + { + //do nothing, we just want to empty the error stream. + } + + + + /// + /// Kill the CLI tool - if running + /// + protected void KillProcess() + { + if (started) + { + if (!process.HasExited) + { + process.Kill(); + } + started = false; + } + process.Dispose(); + } + /// + /// Compute the location of the CLI tool. + /// + /// Returns full path to the CLI tool + protected static string GetToolPath(string relativePath) + { + var rootPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new ArgumentNullException(nameof(Path.GetDirectoryName)); + var toolPath = Path.Combine(rootPath,relativePath); + return toolPath; + } + /// + /// Read data from CLI tool + /// + /// will return empty string if we don't finish reading all data in the timeout provided in milliseconds. + /// + protected virtual async Task GetData(int timeoutms) + { + var readStdOutTask = Task.Run(() => + { + if (process.HasExited) + { + return string.Empty; + } + + using (var writer = new StringWriter()) + { + var done = false; + var start = false; + while (!done) + { + try + { + var line = process.StandardOutput.ReadLine(); + MessageLogged?.Invoke(line); + if (line == null || line == startofDataToken) + { + start = true; + continue; //don't record start token to stream. + } + + if (line == null || line == endOfDataToken) + { + done = true; + } + else + { + //if we have started recieving valid data, start recording + if (!string.IsNullOrWhiteSpace(line) && start) + { + writer.WriteLine(line); + } + } + } + catch (Exception) + { + KillProcess(); + return GetCantCommunicateErrorMessage(); + } + } + + return writer.ToString(); + } + }); + var completedTask = await Task.WhenAny(readStdOutTask, Task.Delay(TimeSpan.FromMilliseconds(timeoutms))); + //if the completed task was our read std out task, then return the data + //else we timed out, so return an empty string. + return completedTask == readStdOutTask ? readStdOutTask.Result : string.Empty; + } + + protected void RaiseMessageLogged(string message) + { + MessageLogged?.Invoke(message); + } + + /// + /// Can't start Error message + /// + /// Returns error message + protected abstract string GetCantStartErrorMessage(); + + + /// + /// Can't communicate Error message + /// + /// Returns error message + protected abstract string GetCantCommunicateErrorMessage(); + + + + } +} diff --git a/src/DynamoUtilities/DynamoFeatureFlagsManager.cs b/src/DynamoUtilities/DynamoFeatureFlagsManager.cs index e4237cbd4b7..d60c886c7c9 100644 --- a/src/DynamoUtilities/DynamoFeatureFlagsManager.cs +++ b/src/DynamoUtilities/DynamoFeatureFlagsManager.cs @@ -24,6 +24,16 @@ internal class DynamoFeatureFlagsManager : CLIWrapper private Dictionary AllFlagsCache { get; set; }//TODO lock is likely overkill. private SynchronizationContext syncContext; internal static event Action FlagsRetrieved; + + //TODO(DYN-6464)- remove this field!. + /// + /// set to true after some FF issue is logged. For now we only log once to avoid clients overwhelming the logger. + /// + private bool loggedFFIssueOnce = false; + /// + /// Timeout in ms for feature flag communication with CLI process. + /// + private const int featureFlagTimeoutMs = 5000; /// /// Constructor @@ -54,18 +64,18 @@ internal void CacheAllFlags() { //wait for response - var dataFromCLI = GetData(); + var dataFromCLI = GetData(featureFlagTimeoutMs).Result; //convert from json string to dictionary. try - { + { AllFlagsCache = JsonConvert.DeserializeObject>(dataFromCLI); //invoke the flags retrieved event on the sync context which should be the main ui thread. syncContext?.Send((_) => - { + { FlagsRetrieved?.Invoke(); - },null); - + }, null); + } catch (Exception e) { @@ -74,34 +84,45 @@ internal void CacheAllFlags() } /// - /// Check feature flag value, if not exist, return defaultval + /// Check feature flag value, if it does not exist, return the defaultval. /// - /// - /// - /// + /// Must be a bool or string, only bool or string flags should be created unless this implementation is improved. + /// feature flag name + /// Currently the flag and default val MUST be a bool or string. /// internal T CheckFeatureFlag(string featureFlagKey, T defaultval) { if(!(defaultval is bool || defaultval is string)){ - RaiseMessageLogged("unsupported flag type"); - return defaultval; + throw new ArgumentException("unsupported flag type", defaultval.GetType().ToString()); } // if we have not retrieved flags from the cli return empty - // and log. + // and log once. if(AllFlagsCache == null) - { - RaiseMessageLogged("the flags cache is null, something went wrong retrieving feature flags," + - " or you need to wait longer for the cache to populate, you can use the static FlagsRetrieved event for this purpose. "); + { //TODO(DYN-6464) Revisit this and log more when the logger is not easily overwhelmed. + if (!loggedFFIssueOnce) + { + RaiseMessageLogged( + $"The flags cache was null while checking {featureFlagKey}, something went wrong retrieving feature flags," + + " or you need to wait longer for the cache to populate before checking for flags, you can use the static FlagsRetrieved event for this purpose." + + "This message will not be logged again, and future calls to CheckFeatureFlags will return default values!!!"); + } + + loggedFFIssueOnce = true; return defaultval; } - if (AllFlagsCache.ContainsKey(featureFlagKey)) + if (AllFlagsCache.TryGetValue(featureFlagKey, out var flagVal)) { - return (T)AllFlagsCache[featureFlagKey]; + return (T)flagVal; } else { - RaiseMessageLogged($"failed to get value for feature flag key ex: {featureFlagKey},{System.Environment.NewLine} returning default value: {defaultval}"); + if (!loggedFFIssueOnce) + { + RaiseMessageLogged( + $"failed to get value for feature flag key ex: {featureFlagKey},{System.Environment.NewLine} returning default value: {defaultval}"); + } + loggedFFIssueOnce = true; return defaultval; } } diff --git a/src/DynamoUtilities/Md2Html.cs b/src/DynamoUtilities/Md2Html.cs index 5d8c4ed1dc7..ceb5908b2a7 100644 --- a/src/DynamoUtilities/Md2Html.cs +++ b/src/DynamoUtilities/Md2Html.cs @@ -1,168 +1,11 @@ using System; -using System.ComponentModel; -using System.Diagnostics; using System.IO; -using System.Reflection; +using System.Threading; using DynamoUtilities.Properties; +using Newtonsoft.Json.Linq; namespace Dynamo.Utilities { - //TODO move to new file. - /// - /// Base class for Dynamo CLI wrappers - /// - internal abstract class CLIWrapper : IDisposable - { - protected const string endOfDataToken = @"<<<<>>>>"; - protected const string startofDataToken = @"<<<<>>>>"; - protected readonly Process process = new Process(); - protected bool started; - internal event Action MessageLogged; - - public virtual void Dispose() - { - process.ErrorDataReceived -= Process_ErrorDataReceived; - KillProcess(); - } - - /// - /// Start the process. - /// - /// relative path to the exe to start. - /// argument string to pass to process. - protected virtual void StartProcess(string relativeEXEPath, string argString) - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardInput = true, - RedirectStandardError = true, - - UseShellExecute = false, - Arguments = argString, - FileName = GetToolPath(relativeEXEPath) - }; - - process.StartInfo = startInfo; - try - { - process.Start(); - started = true; - //the only purspose here is to avoid deadlocks when std err gets filled up 4kb - //in long running processes. - process.ErrorDataReceived += Process_ErrorDataReceived; - process.BeginErrorReadLine(); - - } - catch (Win32Exception) - { - // Do nothing - } - } - - private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e) - { - //do nothing, we just want to empty the error stream. - } - - - - /// - /// Kill the CLI tool - if running - /// - protected void KillProcess() - { - if (started) - { - if (!process.HasExited) - { - process.Kill(); - } - started = false; - } - process.Dispose(); - } - /// - /// Compute the location of the CLI tool. - /// - /// Returns full path to the CLI tool - protected static string GetToolPath(string relativePath) - { - var rootPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new ArgumentNullException(nameof(Path.GetDirectoryName)); - var toolPath = Path.Combine(rootPath,relativePath); - return toolPath; - } - //TODO if we see any issues with deadlocks we can try using a timeout on another thread. - /// - /// Read data from CLI tool - /// Returns data read from CLI tool - /// - protected virtual string GetData() - { - if (process.HasExited) - { - return string.Empty; - } - using (var writer = new StringWriter()) - { - var done = false; - var start = false; - while (!done) - { - try - { - var line = process.StandardOutput.ReadLine(); - MessageLogged?.Invoke(line); - if (line == null || line == startofDataToken) - { - start = true; - continue;//don't record start token to stream. - } - if (line == null || line == endOfDataToken) - { - done = true; - } - else - { //if we have started recieving valid data, start recording - if (!string.IsNullOrWhiteSpace(line) && start) - { - writer.WriteLine(line); - } - } - } - catch (Exception e) - { - KillProcess(); - return GetCantCommunicateErrorMessage(); - } - } - - return writer.ToString(); - } - } - - protected void RaiseMessageLogged(string message) - { - MessageLogged?.Invoke(message); - } - - /// - /// Can't start Error message - /// - /// Returns error message - protected abstract string GetCantStartErrorMessage(); - - - /// - /// Can't communicate Error message - /// - /// Returns error message - protected abstract string GetCantCommunicateErrorMessage(); - - - - } /// /// Utilities for converting Markdown to html and for sanitizing html /// The Md2Html command line tool is used for doing the actual conversion/santizing @@ -175,6 +18,8 @@ protected void RaiseMessageLogged(string message) internal class Md2Html : CLIWrapper { private string relativePath = Path.Combine(@"Md2Html", @"Md2Html.exe"); + private int processCommunicationTimeoutms = 5000; + /// /// Constructor /// Start the CLI tool and keep it around @@ -228,9 +73,9 @@ internal string ParseMd2Html(string mdString, string mdPath) return GetCantCommunicateErrorMessage(); } - var output = GetData(); + var output = GetData(processCommunicationTimeoutms); - return output; + return output.Result; } /// @@ -257,9 +102,9 @@ internal string SanitizeHtml(string content) return GetCantCommunicateErrorMessage(); } - var output = GetData(); + var output = GetData(processCommunicationTimeoutms); - return output; + return output.Result; } /// diff --git a/src/DynamoUtilities/Properties/AssemblyInfo.cs b/src/DynamoUtilities/Properties/AssemblyInfo.cs index c3981334ac4..67a7633b408 100644 --- a/src/DynamoUtilities/Properties/AssemblyInfo.cs +++ b/src/DynamoUtilities/Properties/AssemblyInfo.cs @@ -24,3 +24,5 @@ [assembly: InternalsVisibleTo("DynamoApplications")] [assembly: InternalsVisibleTo("DynamoCLI")] [assembly: InternalsVisibleTo("NodeDocumentationMarkdownGenerator")] +[assembly: InternalsVisibleTo("DynamoUtilitiesTests")] + diff --git a/src/Engine/ProtoAssociative/CodeGen_SSA.cs b/src/Engine/ProtoAssociative/CodeGen_SSA.cs index 745092a71ad..2958837dfc1 100644 --- a/src/Engine/ProtoAssociative/CodeGen_SSA.cs +++ b/src/Engine/ProtoAssociative/CodeGen_SSA.cs @@ -1,4 +1,4 @@ -using ProtoCore.AST.AssociativeAST; +using ProtoCore.AST.AssociativeAST; using ProtoCore.DSASM; using ProtoCore.Utils; using System.Collections.Generic; @@ -275,7 +275,11 @@ private List BuildSSA(List astList, ProtoCore. BinaryExpressionNode bnode = (node as BinaryExpressionNode); int generatedUID = ProtoCore.DSASM.Constants.kInvalidIndex; - if (context.applySSATransform && core.Options.GenerateSSA) + // Skip SSA for input ASTs that are first assigned null such as this: + // a = null; (update "a") => a = ; + // SSA would break up the null AST into two assignments, which then breaks update: + // a = null; (SSA) => temp = null; a = temp; + if (context.applySSATransform && core.Options.GenerateSSA && !bnode.IsInputExpression) { int ssaID = ProtoCore.DSASM.Constants.kInvalidIndex; string name = ProtoCore.Utils.CoreUtils.GenerateIdentListNameString(bnode.LeftNode); @@ -382,53 +386,6 @@ private List BuildSSA(List astList, ProtoCore. return astList; } - private void DfsSSAIeentList(AssociativeNode node, ref Stack ssaStack, ref List astlist) - { - if (node is IdentifierListNode) - { - IdentifierListNode listNode = node as IdentifierListNode; - - bool isSingleDot = !(listNode.LeftNode is IdentifierListNode) && !(listNode.RightNode is IdentifierListNode); - if (isSingleDot) - { - BinaryExpressionNode bnode = BuildSSAIdentListAssignmentNode(listNode); - astlist.Add(bnode); - ssaStack.Push(bnode); - } - else - { - DfsSSAIeentList(listNode.LeftNode, ref ssaStack, ref astlist); - - IdentifierListNode newListNode = node as IdentifierListNode; - newListNode.Optr = Operator.dot; - - AssociativeNode leftnode = ssaStack.Pop(); - Validity.Assert(leftnode is BinaryExpressionNode); - - newListNode.LeftNode = (leftnode as BinaryExpressionNode).LeftNode; - newListNode.RightNode = listNode.RightNode; - - BinaryExpressionNode bnode = BuildSSAIdentListAssignmentNode(newListNode); - astlist.Add(bnode); - ssaStack.Push(bnode); - - } - } - else if (node is FunctionCallNode) - { - FunctionCallNode fcNode = node as FunctionCallNode; - for (int idx = 0; idx < fcNode.FormalArguments.Count; idx++) - { - AssociativeNode arg = fcNode.FormalArguments[idx]; - - Stack ssaStack1 = new Stack(); - DFSEmitSSA_AST(arg, ssaStack1, ref astlist); - AssociativeNode argNode = ssaStack.Pop(); - fcNode.FormalArguments[idx] = argNode is BinaryExpressionNode ? (argNode as BinaryExpressionNode).LeftNode : argNode; - } - } - } - private void DFSEmitSSA_AST(AssociativeNode node, Stack ssaStack, ref List astlist) { Validity.Assert(null != astlist && null != ssaStack); @@ -488,7 +445,8 @@ private void DFSEmitSSA_AST(AssociativeNode node, Stack ssaStac var bnode = AstFactory.BuildAssignment(leftNode, rightNode); bnode.isSSAAssignment = isSSAAssignment; - bnode.IsInputExpression = astBNode.IsInputExpression; + // TODO: SSA is not called for ASTs that are input expressions. Revisit this if there are any issues. + // bnode.IsInputExpression = astBNode.IsInputExpression; astlist.Add(bnode); ssaStack.Push(bnode); diff --git a/src/Engine/ProtoCore/DSASM/InstructionSet.cs b/src/Engine/ProtoCore/DSASM/InstructionSet.cs index 07d285218c4..31068a45323 100644 --- a/src/Engine/ProtoCore/DSASM/InstructionSet.cs +++ b/src/Engine/ProtoCore/DSASM/InstructionSet.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using ProtoCore.Exceptions; @@ -10,6 +10,7 @@ namespace ProtoCore.DSASM public enum Registers { RX, + // Register used to temporarily store primitive values for graph update cycles. LX, } diff --git a/src/Libraries/CoreNodeModels/Enum.cs b/src/Libraries/CoreNodeModels/Enum.cs index a6c1639c352..a2c9860da3c 100644 --- a/src/Libraries/CoreNodeModels/Enum.cs +++ b/src/Libraries/CoreNodeModels/Enum.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; diff --git a/test/DynamoCoreTests/Logging/FeatureFlagTests.cs b/test/DynamoCoreTests/Logging/FeatureFlagTests.cs index 968d6b712a2..7fd1698a6b8 100644 --- a/test/DynamoCoreTests/Logging/FeatureFlagTests.cs +++ b/test/DynamoCoreTests/Logging/FeatureFlagTests.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using System; using System.Diagnostics; +using System.Text.RegularExpressions; using System.Threading; namespace Dynamo.Tests.Logging @@ -69,6 +70,42 @@ public void FeatureFlagsShouldMessageLoggedShouldContainAllLogs() StringAssert.EndsWith("<<<<>>>>", log); } + //TODO(DYN-6464) Revisit this and log more when the logger is not easily overwhelmed. + [Test] + public void FeatureFlagsShouldMessageLoggedShouldOnlyContainNullFlagErrorOnce() + { + var testflagsManager = new DynamoUtilities.DynamoFeatureFlagsManager("testkey", new SynchronizationContext(), true); + testflagsManager.MessageLogged += TestflagsManager_MessageLogged; + testflagsManager.CheckFeatureFlag("TestFlag2", "na"); + testflagsManager.CheckFeatureFlag("TestFlag2", "na"); + testflagsManager.CheckFeatureFlag("TestFlag2", "na"); + testflagsManager.MessageLogged -= TestflagsManager_MessageLogged; + var matches = Regex.Matches(log, "wait longer for the cache").Count; + Assert.AreEqual(1,matches); + } + //TODO(DYN-6464) Revisit this and log more when the logger is not easily overwhelmed. + [Test] + public void FeatureFlagsShouldMessageLoggedShouldOnlyContainMissingFlagErrorOnce() + { + var testflagsManager = new DynamoUtilities.DynamoFeatureFlagsManager("testkey", new SynchronizationContext(), true); + testflagsManager.MessageLogged += TestflagsManager_MessageLogged; + testflagsManager.CacheAllFlags(); + testflagsManager.CheckFeatureFlag("MissingFlag", "na"); + testflagsManager.CheckFeatureFlag("MissingFlag", "na"); + testflagsManager.CheckFeatureFlag("MissingFlag", "na"); + testflagsManager.MessageLogged -= TestflagsManager_MessageLogged; + var matches = Regex.Matches(log, "failed to get value").Count; + Assert.AreEqual(1, matches); + } + [Test] + public void FeatureFlagsThrowsIfCheckIngNonSupportedType() + { + var testflagsManager = new DynamoUtilities.DynamoFeatureFlagsManager("testkey", new SynchronizationContext(), true); + Assert.Throws(() => + { + testflagsManager.CheckFeatureFlag("NumericTypeNotSupported", 10); + }); + } private void DynamoFeatureFlagsManager_FlagsRetrieved() { diff --git a/test/DynamoCoreWpfTests/NodeExecutionUITest.cs b/test/DynamoCoreWpfTests/NodeExecutionUITest.cs index 73bb94e40ca..8662c69d72e 100644 --- a/test/DynamoCoreWpfTests/NodeExecutionUITest.cs +++ b/test/DynamoCoreWpfTests/NodeExecutionUITest.cs @@ -328,5 +328,58 @@ public void TestSelectionNodeUpdate2() AssertPreviewValue(tsn.GUID.ToString(), 0); } + + [Test] + public void TestDropdownNodeUpdate() + { + var model = GetModel(); + var tdd = new TestDropdown + { + SelectedIndex = 0 + }; + + var command = new DynamoModel.CreateNodeCommand(tdd, 0, 0, true, false); + model.ExecuteCommand(command); + + AssertPreviewValue(tdd.GUID.ToString(), "one"); + + tdd.SelectedIndex = 1; + tdd.OnNodeModified(); + + AssertPreviewValue(tdd.GUID.ToString(), "two"); + + tdd.SelectedIndex = 2; + tdd.OnNodeModified(); + + AssertPreviewValue(tdd.GUID.ToString(), "three"); + + } + + [Test] + public void TestDropdownNodeUpdate1() + { + var model = GetModel(); + var tdd = new TestDropdown(); + + var command = new DynamoModel.CreateNodeCommand(tdd, 0, 0, true, false); + model.ExecuteCommand(command); + + AssertPreviewValue(tdd.GUID.ToString(), null); + + tdd.SelectedIndex = 0; + tdd.OnNodeModified(); + + AssertPreviewValue(tdd.GUID.ToString(), "one"); + + tdd.SelectedIndex = 1; + tdd.OnNodeModified(); + + AssertPreviewValue(tdd.GUID.ToString(), "two"); + + tdd.SelectedIndex = 2; + tdd.OnNodeModified(); + + AssertPreviewValue(tdd.GUID.ToString(), "three"); + } } } diff --git a/test/Libraries/DynamoUtilitiesTests/CLIWrapperTests.cs b/test/Libraries/DynamoUtilitiesTests/CLIWrapperTests.cs new file mode 100644 index 00000000000..0657e0b5a70 --- /dev/null +++ b/test/Libraries/DynamoUtilitiesTests/CLIWrapperTests.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace DynamoUtilitiesTests +{ + [TestFixture] + public class CLIWrapperTests + { + /// + /// A test class that starts up the DynamoFF CLI and then kills it to cause a deadlock. + /// + private class HangingCLIWrapper: Dynamo.Utilities.CLIWrapper + { + private string relativePath = Path.Combine("DynamoFeatureFlags", "DynamoFeatureFlags.exe"); + protected override string GetCantStartErrorMessage() + { + throw new NotImplementedException(); + } + + protected override string GetCantCommunicateErrorMessage() + { + throw new NotImplementedException(); + } + internal HangingCLIWrapper() + { + StartProcess(relativePath, null); + } + + internal string GetData() + { + //wait a bit, and then kill the process + //this will cause GetData to hang and timeout. + Task.Run(() => + { System.Threading.Thread.Sleep(100); + process.Kill(); + }); + return GetData(2000).Result; + } + } + + [Test] + public void CLIWrapperDoesNotHangIfProcessDoesNotWriteToStdOut() + { + var sw = new System.Diagnostics.Stopwatch(); + sw.Start(); + var wrapper = new HangingCLIWrapper(); + Assert.AreEqual(string.Empty,wrapper.GetData()); + sw.Stop(); + Assert.GreaterOrEqual(sw.ElapsedMilliseconds,2000); + + } + } +} diff --git a/test/TestUINodes/TestUINodes.cs b/test/TestUINodes/TestUINodes.cs index cd3e3581545..004c2ff971a 100644 --- a/test/TestUINodes/TestUINodes.cs +++ b/test/TestUINodes/TestUINodes.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.IO; @@ -140,4 +140,54 @@ protected override int GetModelObjectFromIdentifer(string id) return id.Length; } } + + [NodeName("Test Dropdown Node")] + [NodeCategory("TestUINodes")] + [NodeDescription("test dropdown node")] + [OutPortTypes("string")] + [IsDesignScriptCompatible] + [IsVisibleInDynamoLibrary(false)] + public class TestDropdown : DSDropDownBase + { + public TestDropdown() : base("TestDropdown") { } + + + public override IEnumerable BuildOutputAst(List inputAstNodes) + { + AssociativeNode node; + if (SelectedIndex < 0 || SelectedIndex >= Items.Count) + { + node = AstFactory.BuildNullNode(); + return new[] { AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), node) }; + } + else + { + // get the selected items name + var stringNode = AstFactory.BuildStringNode((string)Items[SelectedIndex].Name); + + // assign the selected name to an actual enumeration value + var assign = AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), stringNode); + + // return the enumeration value + return new List { assign }; + } + } + + protected override SelectionState PopulateItemsCore(string currentSelection) + { + Items.Clear(); + + var symbols = new[] { "one", "two", "three" }; + + + foreach (var symbol in symbols) + { + + Items.Add(new DynamoDropDownItem(symbol, symbol)); + } + + return SelectionState.Restore; + } + + } }