I ran into .NET (Framework, w/ WinForm and WebForms) / MS SQL project where significant tables in the database contain a TIMESTAMP (aka ROWVERSION) column (called tsModified) to prevent concurrency issues. Additionally, this project does not allow the application layer to interact directly with the tables (instead all CRUD and business logic must be done through Stored Procedures).
One of the things that has been driving me crazy is how to use an SqlDataSource that can account for the TIMESTAMP column during UPDATE.
The basic form of the CRUD procs are like this:
CREATE PROC Customer_List
#pk_Customer INT = null
SELECT id, name, tsModified
FROM Customer
WHERE #pk_Customer IS NULL OR #pk_Customer = id;
CREATE PROC Customer_Update
#id INT,
#name varchar,
#tsMod TIMESTAMP
IF NOT EXISTS (SELECT TOP 1 1 FROM Customer where id=#id and tsModified=#tsModified)
Return; --actually RAISEERROR w/ a concurrency alert telling the user to refresh & try again
UPDATE Customer SET [param/value pairs] WHERE id = #id;
Sure, you could manually define partial classes and methods to account for tsModified, and then use asp:ObjectDataSource, but that is extra work. I just want the convenience of dropping an asp:SqlDataSource on the form, and get on with my day.
BUT... the SqlDataSource does not like the TIMESTAMP as a parameter. In fact, I've literally spent days researching how to make this work, and ran into plenty of others having the same issue.
I finally figured it out. See answer below.
Here's how you can use a MS SQL ROWVERSION (TIMESTAMP) column with an asp:SqlDataSource while using Stored Procedures, to handle concurrency.
Set up your SqlDataSource like this:
<asp:SqlDataSource ID="dsRegs" runat="server" OnUpdating="dsRegs_Updating" ConnectionString="[your connstr]" InsertCommand="RegulatoryAgency_Insert" InsertCommandType="StoredProcedure" SelectCommand="RegulatoryAgency_List" SelectCommandType="StoredProcedure" UpdateCommand="RegulatoryAgency_Update" UpdateCommandType="StoredProcedure">
<InsertParameters>
<asp:Parameter Name="RegulatoryCode" Type="String" />
<asp:Parameter Name="RegulatoryName" Type="String" />
<asp:Parameter Name="RegulatoryState" Type="String" />
</InsertParameters>
<SelectParameters>
<asp:Parameter Name="pk_RegulatoryAgency" Type="DBNull" />
</SelectParameters>
<UpdateParameters>
<asp:Parameter Name="pk_RegulatoryAgency" Type="Int32" />
<asp:Parameter Name="RegulatoryCode" Type="String" />
<asp:Parameter Name="RegulatoryName" Type="String" />
<asp:Parameter Name="RegulatoryState" Type="String" />
<asp:Parameter Direction="InputOutput" Name="tsModified" Type="Empty" />
</UpdateParameters>
</asp:SqlDataSource>
The important things to notice are:
In UpdateParameters, tsModified is the TIMESTAMP value and the Type="Empty".
OnUpdating is set to the dsRegs_Updating event.
Now the code behind:
/// <summary>
/// When editing for this record/row begins in the grid, we need to get the primary key from the row,
/// and then stuff the TIMESTAMP (tsModified) into a Session variable so it persists
/// </summary>
protected void gvRegs_StartRowEditing(object sender, DevExpress.Web.Data.ASPxStartRowEditingEventArgs e)
{
int pk = (int)e.EditingKeyValue;
var db = new myDataContext();
var ra = db.RegulatoryAgency_List(pk).First();
Session["tsModified"] = ra.tsModified;
}
/// <summary>
/// Before we call the database, convert the Session var back the original Linq-to-SQL type (System.Data.Linq.Binary), then
/// convert it to a (byte) array, and update the SqlDataSource parameter with the correct value.
/// </summary>
protected void dsRegs_Updating(object sender, SqlDataSourceCommandEventArgs e)
{
DbParameter dp = e.Command.Parameters["#tsModified"];
dp.Value = ((System.Data.Linq.Binary)Session["tsModified"]).ToArray();
}
In this example, the front is using a DevExpress ASPxGridView, but the databinding and events should be similar on other databound controls. When the row editing begins, we pull the tsModified value for the record from the database and place it into a Session variable. Then the SqlDataSource fires its Updating event, we grab the Session variable, convert it back to it's original format (in my case a System.Data.Linq.Binary because this example is using Linq-to-SQL), and finally the last trick is that you can't pass the TIMESTAMP value as binary, varbinary, or byte -- is must be sent as btye[], which the .ToArray() is taking care.
With the code like this, I'm able to successfully SELECT, INSERT, UPDATE and DELETE through the SqlDataSource, and the tsModified (TIMESTAMP) value in the database increments as expected.
Related
So, I've been fighting with this for more time than I'd like to admit, and can't seem to find info on what I'm doing wrong. So, humbly, I submit this question.
I have 2 dropdowns and datasources as defined here:
<asp:DropDownList ID="ddAdminYear" runat="server" enabled="false" DataTextField="YEAR_ID" DataValueField="YEAR_ID" AutoPostBack="True" AppendDataBoundItems="true">
<asp:ListItem Text="----------" Value="----------" /></asp:DropDownList>
<asp:SqlDataSource ID="sdsAdminDistinctYr" runat="server"
ProviderName="System.Data.SqlClient" SelectCommand="SELECT DISTINCT YEAR_ID FROM PC_YEAR ORDER BY YEAR_ID" DataSourceMode="DataReader"></asp:SqlDataSource>
<asp:DropDownList ID="tbOneUnit" runat="server" AutoPostBack="True"
DataTextField="LONG_DESC" DataValueField="SHORT_DESC_EN" AppendDataBoundItems="True" Enabled="True">
<asp:ListItem Text="----------" Value="----------" /></asp:DropDownList>
<asp:SqlDataSource ID="sdsAdminMunic" runat="server" Onselecting="eventselect" ProviderName="System.Data.SqlClient" SelectCommand="GET_MUNIC_LISTING_VB" SelectCommandType="StoredProcedure" >
<SelectParameters>
<asp:controlparameter DefaultValue="2017" Name="year" controlid="ddAdminYear" propertyname="SelectedValue"/>
<asp:Parameter DefaultValue="default" Name="region" Type="String"/>
<asp:Parameter DefaultValue="default" Name="u_r" Type="String"/>
<asp:Parameter DefaultValue="default" Name="UserGroup" Type="String"/>
</SelectParameters>
</asp:SqlDataSource>
The connectionString for sdsAdminDistinctYr is set when the page loads. The code that runs looks like this:
sdsAdminDistinctYr.ConnectionString = _user.SelectedDBConn
ddAdminYear.DataSourceID = "sdsAdminDistinctYr"
Then, in the select event handler for ddAdminYear, I set the datasource for tbOneUnit as follows:
sdsAdminMunic.ConnectionString = _user.SelectedDBConn
tbOneUnit.DataSourceID = "sdsAdminMunic"
tbOneUnit.DataBind()
The ddAdminYear dropdown populates, however, the tbOneUnit dropdown does not.
How can I get the tbOneUnit dropdown to populate using a stored procedure based on the ddAdminYear value?
EDIT:
I can't post the stored procedure itself, but here's the header:
[dbo].[GET_MUNIC_LISTING_VB] (
#YEAR smallint,
#REGION as varchar(5) = NULL,
#U_R as varchar(2) = NULL,
#USERGROUP as varchar(10) = NULL)
I can verify it returns data.
Let me know if anything else would be helpful.
Edit #2:
The stored procedure is running, but the "default" values are being passed as strings (which I had specified)... So, that's not what I want to do.
Right now the application will pass something like :
exec GET_MUNIC_LISTING_VB #year=N'2017',#region=N'default,N'#u_r=N'default',#UserGroup=N'default'
To the application, whereas I'd like it to pass:
exec GET_MUNIC_LISTING_VB #year=N'2017',#region=default,#u_r=default,#UserGroup=default
I would approach the problem one of two ways depending on how much data you will need for all possible combinations of data for the two drop downs.
If you have a limited number of values for ddAdminYear I would create a tbOneUnit for each value, then I would dynamically show or hide each of these drop downs based on the selected value using some javascript. You would need to create a foreach loop and unique Id or some other identifier so that you could identify each unique drop down.
$(document).ready(function() {
$('#ddAdminYear').change(function() {
var adminYear = $(this).val();
$('.tbOneUnit').hide();
$('#tbOneUnit_' + adminYear).show();
});
});
If you have too much data for solution above, then another solution would be to create an ajax event every time the ddAdminYear changed, and use that to populate the data for tbOneUnit. This of course would require you to write a method on from the server that would return the correct data for tbOneUnit based on the current value of ddAdminYear.
$(document).ready(function() {
$('#ddAdminYear').change(function() {
var adminYear = $(this).val();
var get = $.ajax('/GetTbData', adminYear);
get.done(function(data) {
// take data and add to drop down here
});
});
});
So, it looks like my issue is that I was specifying parameters for executing a stored procedure, when I wanted to use the defaults defined in the stored procedure.
I was able to fix this by commenting out the parameters I had specified, leaving only the ddAdminYear controlParameter. This produced the following code RPC:
exec GET_MUNIC_LISTING_VB #year=N'2017'
Thanks to everyone who posted, and put in an effort.
newbie to asp.net here.
I am trying to setup a selectparameters and controlparameters based off textbox web controls for date ranges to retrieve data on a asp.net page.
SelectCommand="SELECT SUM(Turnover) AS TotalTurnover, (SUM(Turnover) / (SELECT COUNT(*) FROM (SELECT DISTINCT [Trade Date] FROM TradeSummary WHERE ([Trade Date] BETWEEN #T1 AND #T2)))) AS AverageTO FROM TradeSummary WHERE ([Trade Date] BETWEEN #T1 AND #T2)">
<SelectParameters>
<asp:ControlParameter ControlID="TradeDate1" DefaultValue="8-10-2012" Name="T1" Type="DateTime" PropertyName="Text" />
<asp:ControlParameter ControlID="TradeDate2" DefaultValue="8-11-2012" Name="T2" Type="DateTime" PropertyName="Text" />
</SelectParameters>
TradeDate1 and TradeDate2 refer to 2 textbox controls that I setup for date picking.
I am wondering how I can examine the values of #T1 and #T2 so that I can ensure the query being passed to the DB is valid because it seems like if I were to eval("TotalTurnover") then I get a dbnull error.
Any help would be appreciated. Thanks!
you can also pass the control paraamters from your code behind after checking the values. Add the below code in your Page_Load or any other control event ( e.g button )
// check your textbox values
if( TradeDate1.Text != null )
{
SqlDataSource1.SelectParameters.Add("#T1",TradeDate1.Text);
}
I'd check the input values of the textboxes on a click event of a button and if they're all ok go on the databind the datasource. Then on the selecting event of the datasource I'd set the parameters necessary by using datsource.SelectParameters["T1"].DefaultValue = date;
I have a form with text box for date input. If I Provide date everything is fine. If I don't provide date (I leave it empty), DateTime.min (1/1/0001) value is sent. And this causes error: The conversion of a datetime2 data type to a datetime data type resulted in an out-of-range value
My date property is defined as nullable:
public Nullable<System.DateTime> InstallDate { get; set; }
What I want is null value to be written to database if I don't provide date.
I know I could in my update method check for 1/1/0001 and send null, but, do I have to do that? Is there a way around this problem? Thanks
EDIT:
I solved the problem adding code below into my update method:
tank.InstallDate = tank.InstallDate == DateTime.MinValue ? null : tank.InstallDate;
So my code looks like this:
public void UpdateTank(Tank tank)
{
using (RetailFuelEntities ctx = new RetailFuelEntities())
{
tank.InstallDate = tank.InstallDate == DateTime.MinValue ? null : tank.InstallDate;
ctx.Tanks.Attach(tank);
ctx.Entry(tank).State = EntityState.Modified;
ctx.SaveChanges();
}
}
Here is my ObjectDataSource:
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="GetTank"
TypeName="DataAccess.Fuel.EF4.Tanks" DataObjectTypeName="DataAccess.Fuel.EF4.Tank"
DeleteMethod="DeleteTank" InsertMethod="InsertTank" UpdateMethod="UpdateTank">
<SelectParameters>
<asp:ControlParameter ControlID="TextBoxTankId" Name="TankId" PropertyName="Text"
Type="String" />
</SelectParameters>
</asp:ObjectDataSource>
I wonder if null checking can be avoided.
The problem is that your UI control (FormView) doesn't convert an empty string to null, but instead to DateTime.MinValue. Your property happily accepts this value, because it's a valid date after all.
The solution can be found here. Basically, there's a parameter attribute specifically for this case and it's called ConvertEmptyStringToNull.
I have a StoredProcedure called "usp_posts_getall" and it has 1 parameter called "#thisCategoryID"
in my "thisCategoryID", any values other than 0(zero) will return all the records in my Posts table.
Now I have a category menu items and each time I select, I set the value in my Session name called "SelectedCID".
So, How do I ...
Create a SessionParameter Programmatically in SqlDataSource?
UPDATE:
ok. I got it working now.
If it's a session parameter that's used by the SqlDataSource, then you can set the value in the session, e.g in Page_Load():
Session["thisCategoryID"] = theCategoryId;
(am I misunderstanding the question?)
Ok, update:
I think you can create an event handler for the SqlDataSource.OnSelecting event. In that handler, you can access the Parameters collection of the datasource and can add another Parameter to it. I currently cannot test the following code, so it might not be fully correct, but I hope you see the idea:
SqlDataSource1_OnSelecting(SqlDataSourceSelectingEventArgs args)
{
var param = new Parameter("#thisCatagoryID");
param.DefaultValue = Session["SelectedCID"];
SqlDataSource1.SelectParameters.Add(param);
}
Alternatively, you can set the parameter declaratively in the markup, e.g:
<asp:SqlDataSource ...>
<SelectParameters>
<asp:SessionParameter Name="thisCategoryID" SessionField="SelectedCID"
DefaultValue="0" />
...
</SelectParameters>
</asp:SqlDataSource>
I'm using a FormView with an ObjectDataSource and binding using <%# Bind("WhateverProp") %> - and all of my nullable columns are coming back with default values of the type in them.
It appears that the FormView object doesn't have a ConvertEmtpyStringToNull property like the other binding containers do. I've found articles suggesting that this was a bug in VS 2005 / .Net 2.0 - but don't see any saying what the resolution was.
Does anyone have any suggestions as to how I can work around this without just re-capturing all of the fields in the ODS_Inserting event? I'd rather not have to write code to re-bind all of my bound fields on the form just to test for nulls.
Struggled with it too.
For a dropdownlist, I do that:
AppendDataBoundItems="true"
<asp:ListItem Text="" Value=""></asp:ListItem>
For my ObjectDataSource, even thoug my UpdateMethod takes a single parameter, the entity, I add Update params for each Nullable Field of the Entity with convert to NULL
<UpdateParameters>
<asp:Parameter Name="No_Empl_Ferme" Type="Int32" ConvertEmptyStringToNull="true" />
</UpdateParameters>
I do the same for the Insert.
Works fine.
I ended up doing this - kind of a shotgun approach, but in this case all of my empty string values should be nulls. I've also considered using a string array in the code to specify which values should be nulled - and then could just loop thru the string array instead of over all of the values.
protected void RequestItemFormView_ItemInserting(object sender, FormViewInsertEventArgs e)
{
for (int i = 0; i < e.Values.Count - 1; i++)
{
if (e.Values[i].ToString() == string.Empty)
{
e.Values[i] = null;
}
}
}
In your Object DataSource, you need to add InsertParameters for each of your nullable type with the Attribute ConvertEmtpyStringToNull="True" :
<InsertParameters>
<asp:Parameter Name="NullableFieldName" Type="Int32" ConvertEmptyStringToNull="true" />
</InsertParameters>
Quote:
Tonio - i'm not using individual params, but DataObjectTypeName instead. My insert method takes a single param, and that's the business object that I want to have saved back to the database. – Scott Ivey May 1 at 12:57
I've fixed it like this:
protected void FormViewSettings_ItemUpdating(object sender, FormViewUpdateEventArgs e)
{
OrderedDictionary values = e.NewValues as OrderedDictionary;
var personID = values["PersonID"];
if (string.IsNullOrEmpty(personID.ToString()))
{
values.Remove("PersonID");
values.Add("PersonID", null);
}
}
It's a little hack but it works fine.
This way you can set the object property to null instead of string.empty without using the ConvertEmptyStringToNull setting.