<asp:UpdatePanel ID="TagsUPNL" runat="server"> <ContentTemplate> <tr> <td style="vertical-align: top;" class="LeftAlign"> <asp:LinkButton ID="myLB" runat="server" Text="some text" OnClick="MyLB_Click" /> </td> <td style="vertical-align: top;"> Control to update ... </td> </tr> </ContentTemplate> </asp:UpdatePanel>What I intended with the above was that the entire contents of the table row (both tds in other words) would be included in the partial post-back. But due to this odd detail of the UpdatePanel's implementation this is not a valid way to achieve this. The fix was to use two UpdatePanels and a trigger, like so:
<tr> <td style="vertical-align: top;" class="LeftAlign"> <asp:UpdatePanel ID="TagsUPNL" runat="server"> <ContentTemplate> <asp:LinkButton ID="AddTagLBTN" runat="server" Text="Add Tag" OnClick="AddTagLBTN_Click" /> </ContentTemplate> </asp:UpdatePanel> </td> <td style="vertical-align: top;"> <asp:UpdatePanel ID="TagsUPNL2" runat="server"> <Triggers> <asp:AsyncPostBackTrigger ControlID="AddTagLBTN" EventName="Click" /> </Triggers> <ContentTemplate> </ContentTemplate> </asp:UpdatePanel> </td> </tr>And that worked!
Wednesday, April 8, 2009
Declaratively Binding ASP.NET Server Control Properties
Today I had the need to bind the 'Visible' property of an ASP.NET PlaceHolder control to a boolean property defined in my code-behind. This was certainly not the first time I've had to do this but it seems like every time I need to do it I have a hard time remembering the correct way to do it.
So now that I've figured it out (again) I'm going to make a short post here about how to do it, both for my reference and yours.
With client-side controls you can use the <%= %> operators to bind properties to code-behind generated values. For instance, let's say that for some reason I wanted to define the URL to my home page in my code-behind. I could put a property for accessing this value in the code-behind like so:
protected string HomePageUrl { get { return "http://mysite.com/default.aspx"; } }Then in my .aspx file I could create my client-side href tag declaratively like so:
<a href='<%= HomePageUrl %>'>Home</a>That works just fine. But with server-side controls the above method will simply not work. In my case I wanted to bind the Visible property of the PlaceHolder control to a boolean property in my code-behind. Let's assume a simple property that always returns true:
protected bool ShouldBeVisible { get { return true; } }And then we try to use the same technique we used with the anchor tag with the PlaceHolder:
<asp:PlaceHolder ID="PH1" runat="server" Visible="<%= ShouldBeVisible %>" />This builds OK but when we try to load the page we get a Parser Error: "Cannot create an object of type 'System.Boolean' from its string representation '<% ShouldBeVisible %>' for the 'Visible' property." This is causing a problem for two reasons as far as I understand it. Firstly the <%= %> operator returns a string, not a boolean. Secondly the <%= %> operator doesn't get executed until the page is being rendered. This is way too late in the page lifecycle for the property to properly evaluated and applied. Luckily there is a simple way to solve the problem and that is to use the binding operator instead: <%# %>. This operator gets processed during calls to the DataBind method of whatever it's parent is. In my case I was doing this inside a ListView and the call to the ListView's DataBind method was enough to cause the operator to be evaluated. You can however do this anywhere on your page. Just remember that to get the operator to actually be processed you'll need to call Page.DataBind() somewhere. The Page_Load event handler is usually going to be the best place for this probably. So the proper code to bind the Visible property of my PlaceHolder to a value from the code-behind is:
<asp:PlaceHolder ID="PH1" runat="server" Visible="<%# ShouldBeVisible %>" />Hope this helps you out!
Friday, April 3, 2009
Centering Non-Text Content (Like Buttons) In A Div
Recently I was working on a web form and had the need to center a button below a text box on the form. I was using a table-less layout and I have to admit at first I was stumped.
Basically what I had was a div that contained the text box and the button. To center the button I put another div around it and tried setting text-align: center on that div. Of course that didn't work since a button is not text.
So I did some searching and learned that the correct way to do this is to set the width on the div containing the element to be centered and then to set margin: 0px auto;.
The real key to making this work however (and since I didn't see anyone point this out on the sites that helped me figure this out I'm writing this post now) is to set the width of the div containing the button to be just big enough to contain the button. I originally tried setting the width of the button container div to be the width of the parent container div and my button wasn't centered at all. It was only when I set the width of my button container div to be roughly the width of the button that I started to see it moving towards the center. And when you think about this it makes sense; setting the left and right margins to auto allows the browser to center the div when the page is rendered. But if the div is wider than the element being centered then it is the div being centered rather than the element within it. By matching up the sides of the containing div with the edges of the button we force the centering of the div to take the button with it.
Sample code:
<html> <head> <style type="text/css"> #PageContainer { width: 800px; background-color: Wheat; } #CenteredButtonContainer { width: 85px; margin: 0px auto; } </style> </head> <body> <div id="PageContainer"> <p> This is an example paragraph. Isn't it great? </p> <div id="CenteredButtonContainer"> <input type="submit" /> </div> </div> </body> </html>
Wednesday, June 4, 2008
Getting Started With Linq To Sql and Sql Server Compact Edition (CE)
UPDATE 7.21.2008: Well, I'm now in the middle of my third project using Sql Server CE and I've got to say that it's really kind of a piece of shit and I can't in good conscience recommend using it with LINQ or without. It's a shame because having a decent offering in this space would really be a strength for Microsoft but SqlCE just kind of sucks. The tools suck, the IDE support sucks and the database format itself is so chock-full of limitations and oddities that using it is way more trouble than it's worth. My current advice is that if you've got a project you're thinking of using SqlCE on you should either ditch LINQ and use some other embedded database or else just bite the bullet and use the full version of Sql Server. Bummer huh? Old post preserved below:
Today I had a little project to get done that caused me to take a look at using Microsoft's relatively new product, Sql Server 2005 Compact Edition. As near as I can tell CE (as we'll call Compact Edition from here on out) is sort of like a blend between normal Sql Server and MS Access. In terms of its SQL support and syntax it's like Sql Server. But the big difference is that there is no server. Instead CE utilizes a single database file which has an .sdf extension. This file works similarly to the old Access .mdf files in that you can move it around, copy it, etc. and every instance of the file is its own separate database. Accessing this file from code is very like accessing a normal Sql Server instance: you create a SqlCeConnection object with a connection string and then you can use objects that are immediately familiar like SqlCeDataReader, SqlCeCommand, etc. These objects are found in the System.Data.SqlServerCe namespace and operate almost exactly like their normal System.Data.SqlClient coutnerparts.
But the object of this post is twofold: firstly to explain how to create and work with CE .sdf files and secondly to then show how to use Linq To Sql rather than the SqlServerCe data client classes to access that file in an application. Neither of these tasks is particularly difficult but neither is either obvious or (in my opinion) particularly well documented. So hopefully you've searched for something like 'getting started with Linq to Sql and Sql Server CE' or 'using Linq with Sql Server Compact Edition', you've ended up here and hopefully the following will be of service to you.
To get started you'll need to create your CE database (.sdf) file. You can do this either with Visual Studio 2008 or with Sql Server Management Studio Express SP2. If you're using Management Studio you need to be certain you download the SP2 version for CE support. At the time of this writing it can be found here. Note that for CE files Management Studio provides absolutely no benefits that I can see over just using the built in VS2008 Server Explorer tools so for this post I'm going to be using VS2008 to create and manage the db.
So to create the db in VS2008 either create a new executable project (WinForms or Console) or open an existing one. Then in the Solution Explorer pane right-click on the project and choose 'Add . . .' -> 'New Item . . .' from the context menu. This will bring up the Add New Item dialog. You might want to filter the list by click on 'Data' in the top left pane of this dialog. The template you are looking for is called 'Local Database'. Select this and then type a logical name for the database. This name will be the physical file name as well as the default name for your Linq DataContext when we create it. For this example we'll call it 'MyAppDB.sdf'. Click OK and the .sdf file will be added to your project. VS will immediately then prompt you to create a DataSource and DataSet for your new database. Since we're going to be using Linq we don't need either of those so just cancel out of the Data Source Configuration Window.
At this point you should see MyAppDB.sdf listed as a file within your project in the Solution Explorer. If you open the Server Explorer you should also see it listed under Data Connections. Expand the listing under Data Connections in the Server Explorer and you should see sub-folders for Tables and Replication. If you do then you're db is created and ready to go. Now all you need to do is add some tables. You can either use SQL to do this by right-clicking the db entry under Data Connections and selecting 'New Query' or you can use the GUI to do it by right-clicking on the db's Tables sub-folder and choosing 'Create Table'. The GUI's probably a bit easier but it's also pretty limited. It will allow you to create a primary key and an identity field. It will allow you to create indexes and to set default values on fields. And of course it allows you to add, delete and modify the columns in the table. What it won't do however is let you create foreign keys. I have absolutely no idea why this is since CE supports them just fine but that's the way it is.
If you want to create a foreign key you've got to do it with SQL. Just in case you can't remember the syntax it should be something like this:
alter table myChildTable add constraint FK_ParentID foreign key (ParentID) references myParentTable(ParentID) on delete cascadeOnce you've got your db schema all set up how you want it you're ready to generate your Linq DBML file so you can start coding against your database. Once again however for whatever reason Microsoft has failed to implement the most obvious way of getting this done. The DBML designer in VS doesn't have full support for CE. You can't just create a new Linq to Sql classes object in your project and then drag tables from the Server Explorer to the .dbml files designer canvas. In fact, there is no way (as far as I can tell) within VS to create a DBML file for a CE database. To do this you have to use the command line tool SqlMetal.exe. The easiest way to get at SqlMetal in my opinion is to go into your start menu and from there choose 'Microsoft Visual Studio 2008' -> 'Visual Studio Tools' -> 'Visual Studio 2008 Command Prompt'. This will open a console window whose path and everything are set up correctly to run SqlMetal. In this console window you then need to execute a command like this:
sqlmetal /dbml:MyAppDB.dbml MyAppDB.sdf /pluralizeSqlMetal is the code generator that VS uses to create a DBML file and its code-behinds when you use the designer. While the designer doesn't fully support CE, SqlMetal does. So what we're doing here is an end-run around the designer. Let's take a look at the arguments we're passing to the SqlMetal executable above. When we specify
'/dbml:MyAppDB.dbml'we are telling SqlMetal two things. First we're telling it that we want it to generate a DBML file. Second we're telling it that the filepath of the newly generated file should be
. The second argument,\MyAppDB.dbml
'MyAppDB.sdf'is the filepath to the CE database file that has contains the table schema we want SqlMetal to run against. The final argument,
'/pluralize'tells SqlMetal to automatically handle pluralization of the names of entities. For instance the entity generated to represent rows in a table named Employees will automatically be called Employee. This works pretty well and I recommend using it as it makes your code more readable. In American English at least. Now one thing you'll note is that I didn't specify paths to either the .dbml file or the .sdf file in the above call. You certainly could, but I find it easier to just cd to your project directory first, before you execute SqlMetal at all. That way everything just refers to the current directory (your project directory) by default. So, so far your steps were to:
- Create or open a project in Visual Studio 2008
- Add a Local Database file to that project
- Added one or more tables to the database created in step 2
- Opened a Visual Studio Command Prompt window and changed directory to your project directory
- Ran the SqlMetal command line tool with something like this:
'sqlmetal /dbml:MyAppDB.dbml MyAppDB.sdf /pluralize'
using (MyAppDB db = new MyAppDB("MyAppDB.sdf")) { //add a row to the database MyTestEntity myEntity = new MyTestEntity(); myEntity.TestProperty = "Test"; db.MyTestEntities.InsertOnSubmit(myEntity); db.SubmitChanges(); //load rows from the database into objects var query = from mte in db.MyTestEntities select mte; //should output one instance of the word 'Test' foreach (MyTestEntity e in query) { Console.WriteLine(e.TestProperty); } }Hopefully someone out there finds this helpful! If so leave a comment and let me know.
Tuesday, April 8, 2008
Bulk Deletes in Linq to Sql
UPDATE: Terry Aney independently came up with a similar solution to this problem as I did. Terry however has a better implementation in that his supports compound primary keys. He's also got similar support for batch updates in there. I recommend that you read about his approach here but then read his follow-up and download a later version of his code here. Thanks Terry!
A couple of days ago I was working on an application that uses Linq for its data access layer and I was trying to figure out the Linq way of doing a bulk delete. To illustrate the type of operation I was attempting let's imagine a simple HR database. This database has two tables: Employees and EmployeeTypes. A relationship between the tables exists such that each Employee has one and only one type while each EmployeeType has 0, 1 or many Employees. This is accomplished with the following schema:
Employees
========
EmployeeID int(primary key, identity)
EmployeeName varchar
EmployeeTypeID int (foreign key to EmployeeTypes)
EmployeeTypes
============
EmployeeTypeID int (referenced by Employees)
TypeDesc varchar
Let's say that in my imaginary organization we had been experimenting with using offshore developers to decrease our development costs. Now let's imagine that (just like in most attempts at off shoring development) the higher ups have realized that the code coming from across the pond is total crap and that you really do get what you pay for. Therefore the powers that be have decided to can the whole offshore team. Now we need to update the database to get rid of all the offshore employees in the database. (In reality we probably wouldn't really want to delete these rows but this is just an example.)
In Sql this would be a simple thing to accomplish. We could simply execute the following query:
>. While I think this cuts down on the readability of the consuming code I figured no big deal and so I tried my delete query with this library like so:
that actually does the work:
DELETE FROM Employees WHERE EmployeeTypeID IN (SELECT EmployeeTypeID FROM EmployeeTypes WHERE TypeDesc = 'Offshore')Much to my surprise there is no clear analog to this query in Linq. I thought that this would work:
var toDelete = from e in db.Employees where e.EmployeeType.TypeDesc == "Offshore" select e; db.Employees.DeleteAllOnSubmit(toDelete); db.SubmitChanges();In fact it does work but when I ran the Sql Profiler I noticed something quite troubling. When Linq executed this code it was first running the select defined by my toDelete var and retrieving the results. It was then going through each Employee returned by the query and issuing a SEPARATE delete call for each. To me this seemed needlessly inefficient so I started Googling for a solution. I came up with two hits that attempted to solve this problem. The first was from Chris Rock (no not that Chris Rock.) He wrote a good post about a solution to the problem that a guy named Benjamin Eidelman wrote. You can see this solution at Chris's blog, RocksThoughts. But there were some problems with this solution. Firstly the code he posted did not build as posted. I traced down the problem to the fact that the capture groups in the regex he was using to parse the query were not named properly. I'm guessing that since .NET regexes put their capture group names between less than/greater than symbols his blogging app is stripping out the group names thinking that they are HTML. Fair enough. I've contacted him and hopefully he'll get the fix I supplied posted. A much bigger problem for me though was that when I tried to use his code to accomplish the above defined task I got an error that my query type wasn't supported. I tried to use his code like so:
db.DeleteAll(db.Employees.Where(e => e.EmployeeType.TypeDesc == "Offshore"));
This is definitely what I wanted but in examining the code from Chris's blog I quickly determined that it would only work if the Where clause was very simple and limited to checking values on the object being deleted. Joins are not supported by this code at all as far as I can see. And in fact while looking at the underlying code I realized that it relies heavily on string manipulation to achieve its results. And that certainly doesn't seem very Linq-y to me. Linq gives us the facility to pass around a query as a logical construct so it seemed to me that we should be operating on that construct rather than its interpreted string representation.
So from Chris's blog I then followed a link to a Lambda expression-based solution to the same problem. The solution was from Jeffrey Zhao's blog. Here I thought I would find what I was looking for. A method that examined an IQueryable construct and parsed it into an efficient Sql DELETE statement. And yet upon further examination I found that this wasn't really what I was looking for either. First of all, rather than taking an IQueryable as an argument it takes a predicate defined as Expression
db.Employees.Delete(e => e.EmployeeType.TypeDesc == "Offshore");
This built but when I ran it it the generated query did not join in the EmployeeType table. Instead the query was looking for a field called TypeDesc on the Employees table. So of course it failed. I came to the conclusion that while this method was probably somewhat better than Mr. Eidelman's string manipulations it still didn't do what I wanted.
If you look on the message boards, etc. you will see people asking this question and the answer is usually either to use one of the two above methods or else to write a custom stored procedure to accomplish this type of delete. But having to write a stored proc for every different type of bulk delete I might need to perform just rubbed me the wrong way. I wondered if it would be possible for me to come up with a solution more to my liking.
After some thought I came up with something that so far I'm liking quite a bit. It takes its inspiration from the solutions explored above, combining the legibility of defining the records to delete in a simple Linq query with the relative type safety of using the expression tree to generate the where clause.
Now it doesn't work in all situations.The main one I see right now is that it doesn't support tables with compound primary keys. But other than that I think it's pretty nice. The usage is pretty straightforward:
var toDelete = from e in db.Employees where e.EmployeeType.TypeDesc == "Offshore" select e.EmployeeID; db.Employees.BulkDelete(toDelete);Note that the query is selecting a list of the ids of the records to be deleted. This is key because basically what the code does is wrap the select query defined in Linq inside a delete query based on the primary key of the table being deleted from. So the query that is actually executed on the server from the above is:
delete from Employees where EmployeeID in (select EmployeeID from Employees t0 inner join EmployeeTypes t1 on t0.EmployeeTypeID = t1.EmployeeTypeID where t1.TypeDesc = 'Offshore')Unfortunately I haven't yet been able to come up with a way to make the compiler ensure that the enumerable type returned by the user-supplied query matches the type of the primary key. That's just up to the developer right now. I'd love to hear if anyone's got any ideas beyond just making it only work with int. It should I think at the very least also support string and guid keys. But I think that the Sql Optimizer should be able to execute this sort of query quickly and efficiently. If anyone sees any problems please let me know! Here is the code for the extension method
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Linq; using System.Data.Linq.Mapping; using System.Data; namespace LinqExtensions { public static class TableExtensions { public static int BulkDelete<TEntity>(this Table<TEntity> table, IQueryable idsToDelete) where TEntity : class { MetaTable metaTable = table.Context.Mapping.GetTable(typeof(TEntity)); string tableName = metaTable.TableName; if (metaTable.RowType.IdentityMembers.Count != 1) { var keyFields = from im in metaTable.RowType.IdentityMembers select im.MappedName; if (keyFields == null || keyFields.Count() < 1 ) { keyFields = new List<string>(); } throw new ApplicationException(string.Format("Error in TableExtensions.BulkDelete<TEntity>: Table {0} has a compound key. BulkDelete can only operate on tables with a single key field. (key fields found: {1}", tableName, string.Join(",", keyFields.ToArray()))); } string primaryKey = metaTable.RowType.IdentityMembers[0].MappedName; System.Data.Common.DbCommand origCmd = table.Context.GetCommand(idsToDelete); string toDelete = origCmd.CommandText; string sql = string.Format("delete from {0} where {1} in ({2})", tableName, primaryKey, toDelete); origCmd.CommandText = sql; bool openedConn = false; if (origCmd.Connection.State != ConnectionState.Open) { openedConn = true; origCmd.Connection.Open(); } try { return origCmd.ExecuteNonQuery(); } finally { if (openedConn) { origCmd.Connection.Close(); } } } } } Post in the comments and let me know how it works for you!
Thursday, April 3, 2008
The REAL Current Directory in .NET
I've got this little open source C# app I wrote called PowerFlag. PowerFlag is a response to the unending plethora of spammers on Craigslist. Now, I love Craigslist but the spammers really get on my nerves. So I wrote this app to automatically flag as spam posts which contain keywords that the user defines.
The app worked pretty great except for the fact that when I tried to run it through the Windows
Scheduler it seemed to run fine judging by the logs but it never seemed to find any posts to flag. For instance if I ran the app by double-clicking it and clicked the Flag! button in the app it would find maybe 10 or so spam posts and flag them. This would be evident via a text box in the app and the log file that is created on every run. If I right-clicked the scheduled task and chose 'Run' though the log file would be created but it would say that no posts were found to flag.
I lived with it this way for a couple of months before today I finally got around to figuring out what the root of the problem was and how to fix it. My problem was that the app uses two settings files and the definitions for the filepaths for those two files were stored in string constants which concatenated together System.Environment.CurrentDirectory with the file names of the files. By adding more logging I was able to see that when the app was run via the task scheduler the value of System.Environment.CurrentDirectory was not the directory the app executable was in but instead was for some reason c:\windows\system32. Upon further investigation I found different settings files for the app in this location as well. These settings files contained the initial settings that the program uses on first startup and did not contain all of the keywords I had defined using the app itself.
The fix for this problem of System.Environment.CurrentDirectory not always being the same as the current directory of the executing app is to instead use this:
System.IO.Path.GetDirectoryName(System.Reflection.Assembly._
GetExecutingAssembly().Location)
This is pretty much guaranteed to always give you the path to the folder containing the current executable.
Once I made this change my log files overflowed with flagged posts from the scheduled app. Voila!
Tuesday, March 11, 2008
How To Copy A SqlServer 2005 Database
So today I started a new project here at the office. I'm going to update our homebuilt CMS to include some new functionality that our sites require. The changes I plan to make will require changes to the database structure.
Now the current version of our app's database is running on SqlServer 2005. Since other developers will continue work on the old app while I work on the new one it was necessary for me to create a copy of the database and do my development against this copy.
The problem was that every time I need to make a copy of a database I have a hard time remembering the right way to go about it. Searching on Google was no help. I got links to a bunch of 3rd party crapware and out of date MSDN articles. None of which helped.
Eventually I figured out a good way to do it and figured I'd post it here for anyone else who might need to do this. What I'm describing here is a way to create a duplicate of an existing database on the same server as the existing db. This method uses Sql Server Management Studio Express. I'm sure the full version would work as well. I imagine a similar procedure would work across servers too.
The first step I did was to create a new database on the server which would be the duplicate db. For the purposes of this article let's assume that my original db is called 'Prod' and my new db is called 'Dev'. So I right clicked on the 'Databases' folder in SSMSE (Sql Server Management Studio Express), chose 'New Database . . .' and created a new db called 'Dev'.
Next up I right-clicked on the 'Prod' db (the one I want to duplicate) and chose 'Tasks -> BackUp . . .' I created a full backup of the db just to make sure I had the latest data.
Once the backup had completed I then right-clicked on the 'Dev' database and chose 'Tasks -> Restore -> Database . . .' On the 'General' page I specified 'Prod' as my 'From database' and 'Dev' as my 'To database'. Note that when you select the 'From database' the 'To database' value gets changed to that value. You need to make sure that the 'To database' value is actually the name of the duplicate database.
Next I clicked over to the Options page in the Restore dialog. Here I had to change the 'Restore As' names of the files in the file listing. By default they are the same as the 'Original File Name' values. But that would cause the backup to overwrite the original database. What we want is to change the 'Restore As' names to be the names of the duplicate database's files. In this example I changed these two entries:
C:\Program Files\Microsoft SQL Server\MSSQL\data\Prod.mdf
C:\Program Files\Microsoft SQL Server\MSSQL\data\Prod_log.ldf
To:
C:\Program Files\Microsoft SQL Server\MSSQL\data\Dev.mdf
C:\Program Files\Microsoft SQL Server\MSSQL\data\Dev_log.ldf
Then, still on the Options page of the 'Restore Database' dialog, I checked the 'Overwrite the existing database' checkbox. Next just hit the 'OK' button and let it do its thing.
The end result should be a duplicate of your original database! Hope this helps some of you out there.
So first I created a new, empty db called 'Dev'.
Subscribe to:
Posts (Atom)