1

I have a GetDynamicParameters() on cmdlet Get-DateSlain that does something like this:

    public object GetDynamicParameters()
    {
        List<string> houseList = {"Stark", "Lannister", "Tully"};

        var attributes = new Collection<Attribute>
            {
                new ParameterAttribute
                {
                    HelpMessage = "Enter a house name",
                },
                new ValidateSetAttribute(houseList.ToArray()),
            };

        if (!this.ContainsKey("House"))
        {
            this.runtimeParameters.Add("House", new RuntimeDefinedParameter("House", typeof(string), attributes));
        }
    }

And this works as expected - users can type Get-DateSlain -House, and tab through the available houses. However, once a house is chosen, I want to be able to narrow down the results to characters in that house. Furthermore, if it's house 'Stark', I want to allow a -Wolf parameter. So to implement (some value validity checks removed for brevity):

    public object GetDynamicParameters()
    {
        if (this.runtimeParameters.ContainsKey("House"))
        {
            // We already have this key - no need to re-add. However, now we can add other parameters
            var house = this.runtimeParameters["House"].Value.ToString();
            if (house == "Stark")
            {
                List<string> characters = { "Ned", "Arya", "Rob" };
                var attributes = new Collection<Attribute>
                {
                    new ParameterAttribute
                    {
                        HelpMessage = "Enter a character name",
                    },
                    new ValidateSetAttribute(characters.ToArray()),
                };

                this.runtimeParameters.Add("Character", new RuntimeDefinedParameter("Character", typeof(string), attributes));

                List<string> wolves = { "Shaggydog", "Snow", "Lady" };
                var attributes = new Collection<Attribute>
                {
                    new ParameterAttribute
                    {
                        HelpMessage = "Enter a wolf name",
                    },
                    new ValidateSetAttribute(wolves.ToArray()),
                };

                this.runtimeParameters.Add("Wolf", new RuntimeDefinedParameter("Wolf", typeof(string), attributes));
            }
            else if (house == "Lannister")
            {
                List<string> characters = { "Jaimie", "Cersei", "Tywin" };
                // ...
            }
            // ...

            return this.runtimeParameters;
        }

        List<string> houseList = {"Stark", "Lannister", "Tully"};

        var attributes = new Collection<Attribute>
        {
            new ParameterAttribute
            {
                HelpMessage = "Enter a house name",
            },
            new ValidateSetAttribute(houseList.ToArray()),
        };

        this.runtimeParameters.Add("House", new         RuntimeDefinedParameter("House", typeof(string), attributes));
    }

This looks like it should work, but it doesn't. The GetDynamicParameters function is only called once, and that is before a value is supplied to this.runtimeParameters["House"]. Since it doesn't re-evaluate after that value is filled in, the additional field(s) are never added, and any logic in ProcessRecord that relies on these fields will fail.

So - is there a way to have multiple dynamic parameters that rely on each other?

Rollie
  • 4,391
  • 3
  • 33
  • 55

1 Answers1

2

Have a look a the aswer to this question, it shows a way to access the values of other dynamic parameters in the GetDynamicParameters method: Powershell module: Dynamic mandatory hierarchical parameters

I adapted the code from the mentioned answer so it can handle SwitchParameters and the raw input parameter is converted to the actual type of the cmdlet parameter. It does not work if the dynamic parameter you want to get the value for is passed via pipeline. I think that is not possible because dynamic parameters are always created before pipeline input is evaluated. Here it is:

public static class DynamicParameterExtension
{
    public static T GetUnboundValue<T>(this PSCmdlet cmdlet, string paramName, int unnamedPosition = -1))
    {
        var context = TryGetProperty(cmdlet, "Context");
        var processor = TryGetProperty(context, "CurrentCommandProcessor");
        var parameterBinder = TryGetProperty(processor, "CmdletParameterBinderController");
        var args = TryGetProperty(parameterBinder, "UnboundArguments") as System.Collections.IEnumerable;

        if (args != null)
        {
            var isSwitch = typeof(SwitchParameter) == typeof(T);

            var currentParameterName = string.Empty;
            object unnamedValue = null;
            var i = 0;
            foreach (var arg in args)
            {
                var isParameterName = TryGetProperty(arg, "ParameterNameSpecified");
                if (isParameterName != null && true.Equals(isParameterName))
                {
                    var parameterName = TryGetProperty(arg, "ParameterName") as string;
                    currentParameterName = parameterName;
                    if (isSwitch && string.Equals(currentParameterName, paramName, StringComparison.OrdinalIgnoreCase))
                    {
                        return (T)(object)new SwitchParameter(true);
                    }
                    continue;
                }

                var parameterValue = TryGetProperty(arg, "ArgumentValue");

                if (currentParameterName != string.Empty)
                {
                    if (string.Equals(currentParameterName, paramName, StringComparison.OrdinalIgnoreCase))
                    {
                        return ConvertParameter<T>(parameterValue);
                    }
                }
                else if (i++ == unnamedPosition)
                {
                    unnamedValue = parameterValue;
                }

                currentParameterName = string.Empty;
            }

            if (unnamedValue != null)
            {
                return ConvertParameter<T>(unnamedValue);
            }
        }

        return default(T);
    }

    static T ConvertParameter<T>(this object value)
    {
        if (value == null || Equals(value, default(T)))
        {
            return default(T);
        }

        var psObject = value as PSObject;
        if (psObject != null)
        {
            return psObject.BaseObject.ConvertParameter<T>();
        }

        if (value is T)
        {
            return (T)value;
        }
        var constructorInfo = typeof(T).GetConstructor(new[] { value.GetType() });
        if (constructorInfo != null)
        {
            return (T)constructorInfo.Invoke(new[] { value });
        }

        try
        {
            return (T)Convert.ChangeType(value, typeof(T));
        }
        catch (Exception)
        {
            return default(T);
        }
    }

    static object TryGetProperty(object instance, string fieldName)
    {
        if (instance == null || string.IsNullOrEmpty(fieldName))
        {
            return null;
        }

        const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public;
        var propertyInfo = instance.GetType().GetProperty(fieldName, bindingFlags);

        try
        {
            if (propertyInfo != null)
            {
                return propertyInfo.GetValue(instance, null);
            }
            var fieldInfo = instance.GetType().GetField(fieldName, bindingFlags);

            return fieldInfo?.GetValue(instance);
        }
        catch (Exception)
        {
            return null;
        }
    }        
}

So for your example you should be able to use it like:

public object GetDynamicParameters()
{
    var houseList = new List<string> { "Stark", "Lannister", "Tully" };

    var attributes = new Collection<Attribute>
    {
        new ParameterAttribute { HelpMessage = "Enter a house name" },
        new ValidateSetAttribute(houseList.ToArray()),
    };
    var runtimeParameters = new RuntimeDefinedParameterDictionary
    {
        {"House", new RuntimeDefinedParameter("House", typeof (string), attributes)}
    };

    var selectedHouse = this.GetUnboundValue<string>("House");

    //... add parameters dependant on value of selectedHouse

    return runtimeParameters;
}

After all I'm not sure if it's a good idea trying to get those dynamic parameter values in the first place. It is obviously not supported by PowerShell Cmdlet API (see all the reflection to access private members in the GetUnboundValue method), you have to reimplement the PowerShell parameter conversion magic (see ConvertParameter, I'm sure I missed some cases there) and there is the restriction with pipelined values. Usage at your own risk:)

Community
  • 1
  • 1
Alex
  • 293
  • 2
  • 10
  • Please copy over the relevant code parts if that's at all possible – Johannes Jander Feb 04 '16 at 09:38
  • I added a modified version of the code that just gets unbound parameter values. Unfortunately it didn't get much shorter, but I think it does the trick. – Alex Feb 05 '16 at 19:58