1

I've got a page in my application that will allow users to run stored procedures against the database so that they can manage their data or make changes without using SSMS. Using the SqlCommandBuilder.DeriveParameters method, I was able to successfully fetch and display all stored procedures in a select tag as well as display their input & output parameters.

Here is the contract class:

public class DatabaseUpdateModel
{
    public int ClientId { get; set; }
    public string StoredProcedureName { get; set; }
    public List<SelectListItem> StoredProcedureNames { get; set; }
    public Dictionary<string, SqlParamCollection> StoredProcedures { get; set; }
    public Dictionary<int, string> ParameterInputs { get; set; }


    public DatabaseUpdateModel()
    {
        StoredProcedureNames = new List<SelectListItem>();
        StoredProcedures = new Dictionary<string, SqlParamCollection>();
        ParameterInputs = new Dictionary<int, string>();
    }
}

I do this in two steps - step 1 happens on the controller's index loading to fetch the stored procedure names:

Service methods:

    private DatabaseUpdateModel GetDatabaseUpdateStoredProcedureNames(string connectionString)
    {
        _logger.LogTrace("Begin GetDatabaseUpdateModel service method");
        SqlConnection connection = new SqlConnection(connectionString);
        DBConnectionSetter.SetDBConnection(_SqlHelper, connectionString);

        DatabaseUpdateModel model = new DatabaseUpdateModel();
        IDataReader reader = _SqlHelper.GetDataReader("SELECT name FROM sys.objects WHERE type = 'p' ORDER BY name ASC", CommandType.Text, _appSettings.CommandTimeoutInSeconds, out connection, null);

        while (reader.Read())
        {
            SelectListItem storedProcedure = new SelectListItem
            {
                Value = ((string)reader["name"]).Trim(' '),
                Text = (string)reader["name"]
            };

            model.StoredProcedureNames.Add(storedProcedure);
        }
        reader.Close();

        return model;
    }

Step 2 happens via Ajax when a stored procedure is selected

 public DatabaseUpdateModel GetDatabaseUpdateProcedureParameters(string connectionString, string storedProcedureName)
    {
        _logger.LogTrace("Begin GetDatabaseUpdateProcedureParameters service method");
        SqlConnection connection = new SqlConnection(connectionString);
        DBConnectionSetter.SetDBConnection(_SqlHelper, connectionString);
        DatabaseUpdateModel model = GetDatabaseUpdateStoredProcedureNames(connectionString);
        connection.Open();

        Contract.Shared.SqlParamCollection collection = new Contract.Shared.SqlParamCollection();
        SqlCommand command = new SqlCommand();
        command.Connection = connection;
        command.CommandText = storedProcedureName;
        command.CommandType = CommandType.StoredProcedure;
        SqlCommandBuilder.DeriveParameters(command);
        foreach (SqlParameter param in command.Parameters)
        {
            if (param.Direction == ParameterDirection.Input)
            {
                collection.InputParameters.Add($"{param.ParameterName} - {param.SqlDbType}");
                collection.SqlParameters.Add(param);
            }
            else
            {
                collection.OutputParameters.Add($"{param.ParameterName} - {param.SqlDbType}");
            }
        }

        model.StoredProcedures.Add(storedProcedureName, collection);
        connection.Close();
        return model;
    }

What this leaves me with is this: Showing our table

Now I've been following various articles and resources to get to this point but now I'm having issues correctly binding these inputs to a model to be submitted either by an action or via ajax. Here's my current code in the view (the commented out line is one of my latest attempts to get this working).

@model DatabaseUpdateModel

<form>
    <table id="summaryTable" class="table">
        <thead>
            <tr>
                <td>Input Parameters</td>
                <td>Output Parameters</td>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <ul>
                        @if (!string.IsNullOrEmpty(Model.StoredProcedureName))
                        {
                            for (int i = 0; i < Model.StoredProcedures[Model.StoredProcedureName].InputParameters.Count(); i++)
                            {
                        <li>
                            @Model.StoredProcedures[Model.StoredProcedureName].InputParameters[i].ToString()<br />
                            @*<input id="databaseUpdateParam[@i]" name="parameter[@i]" asp-for="@Model.ParameterInputs[@i]" type="text" />*@
                            <input id="databaseUpdateParam[@i]"type="text" />
                        </li>
                            }
                        }
                    </ul>
                </td>
                <td>
                    <ul>
                        @if (!string.IsNullOrEmpty(Model.StoredProcedureName))
                        {
                            foreach (var param in Model.StoredProcedures[Model.StoredProcedureName].OutputParameters)
                            {
                                <li>
                                    @param
                                </li>
                            }
                        }
                    </ul>
                </td>
            </tr>
        </tbody>
    </table>
</form>
@if (Model.StoredProcedures[Model.StoredProcedureName].InputParameters.Count() > 0)
{
    <button type="submit" asp-action="Foo">Submit</button>
}

Ajax function that runs when a value is selected:

function selectDatabaseUpdateStoredProcedure(clientId, storedProcedureName) {
    var selected = $("#clientStoredProcedurePicker option:selected").val();
    console.log(selected); // Selected Stored Procedure Name
    $.ajax({
        url: "/Process/GetDatabaseUpdateProcedureParameters?ClientId=" + clientId + "&StoredProcedureName=" + storedProcedureName,
        success: function (data) {
            console.log(data);
            $("#summaryTableDiv").html(data);
        }
    });
}

My assumption, and everything that I read up to this point has said that I should be able to simply be able to use the name attribute to denote the dictionary's key and the value in the tag would correspond to the value entered upon form submission. However all of my attempts to achieve this are met with errors (for example that the index is not present in the dictionary).

How can this be done either through a controller action or ajax?

Mkalafut
  • 729
  • 2
  • 13
  • 34
  • https://stackoverflow.com/questions/5191303/asp-net-mvc-binding-to-a-dictionary\ – Seabizkit Oct 29 '20 at 20:18
  • @Seabizkit Yeah, I saw that post and tried something very similar. I tried changing my dictionary from to and changed my input tag helper many times in an attempt to get the values submitted. I'm currently reading through the linked blog post now. – Mkalafut Oct 29 '20 at 20:20
  • @Seabizkit Well I feel like an idiot. Posting my answer below. – Mkalafut Oct 29 '20 at 20:27
  • glad you sorted it, yeah i could just do it for you, but you learn so much more when u fight with it and then wen you figure it out your like yippy, I was just trying to make your you had a link to follow something, i memember when i was trying to figure this out almost 10 yrs ago... why less links and i also tried what felt like a million ways. once you know how it works, you can model bind to other dynamic type lists.... – Seabizkit Oct 29 '20 at 20:49
  • Yeah that was one of the first links I stumbled on. I wrote the question up before I read it because it was something I'd been struggling with the last day or two. :) – Mkalafut Oct 29 '20 at 20:56

1 Answers1

0

I wanted to have this question up so that if someone more knowledgeable happened upon it, I could maybe gain some insight to save myself even more hours of scratching my head. After reading through an article linked in this question I found that I was missing a key piece in how the HTML is rendered and came up with something like this that now correctly posts the values in the inputs. I still need to process and handle the other missing pieces from the model but at least this gets me what I need to move forward.

Here is the relevant code that allows this to work. Creating a hidden input to store the ID on its own and allowing the user to enter a string value seems to work exactly as I expect and want it to.

<td>
  <ul>
    @if (!string.IsNullOrEmpty(Model.StoredProcedureName))
    {
       for (int i = 0; i < Model.StoredProcedures[Model.StoredProcedureName].InputParameters.Count(); i++)
       {
         <li>
              @Model.StoredProcedures[Model.StoredProcedureName].InputParameters[i].ToString()<br />
              <input hidden id="databaseUpdateParamKey[@i]" name="ParameterInputs[@i].Key" value="@i" />
              <input id="databaseUpdateParamValue[@i]" name="ParameterInputs[@i].Value" type="text" />
         </li>
        }
     }
  </ul>
</td>
Mkalafut
  • 729
  • 2
  • 13
  • 34