06 June, 2008

ESRI & ASP.NET Master Pages

One of the great things about VS 2005 & ASP.NET 2.0+ is the ability to quickly and easily create and user Master Pages in your ASP.NET web application.

One of the horribly difficult things about Master Pages is when using mash-ups, figuring out how they work exactly. Ok, if you don't know about Master Pages, then you should probably investigate them some.

A Master Page is not really a page, but rather a template that holds containers into which pages render (but can also regular HTML there as well). So a Master Page can have your title, image banner, navigation links, login stuff, breadcrumb trail...all that stuff, and also contains one or more "ContentPlaceHolder" controls.

The problem when putting an ArcGIS Server map in a page that has a master page is that when the page is rendered, the name of the map changes. That is, if you leave you map the default "Map1" name, when rendered, it has the name "ct00_ContentPlaceHolder1_Map1". Assuming, of course, you lave the ContentPlaceHolder name the default AND you put the map in the first instance of the ContentPlaceHolder controls.

This creates issue when attempting to access the map via JavaScript. In the WebMapApp.js file that is defaultly created when a Web Mapping Application is created in VS2005, it is assuming that you leave everything as the default names when you put stuff in the map. With that in mind, it uses names like "Map1", "TaskResults1" etc.

To fix this, I created a global var in the javascript file:

var webMapControlPrefix = "ctl00_ContentPlaceHolder1_";



So, for instance, the setpageElementSizes() function goes to define the scale bar , it used to do:



webMapAppScaleBar = document.getElementById("ScaleBar1");



But now, I have it looking like this:



webMapAppScaleBar = document.getElementById(webMapControlPrefix+"ScaleBar1");



You probably get the idea. If not, drop a comment and I'll see what I can find out about whatever confusion there is.







21 April, 2008

GridView RowUpdating: Stupid, stupid stupid error

Making an error in your code is one thing. But making an error that is so brilliantly, blatantly and egregiously stupid...well, that is something wholly and completely different.

Case in point. I have a GridView control on a page. I need to be able to edit that GridView control. However, to populate the GridView, I need a dataset which contains a " WHERE [COLUMN] IN (values)" where clause. The problem is, that the SqlDataSource really doesn't seem to support this very easily. I'm almost-but-not-quite certain it probably-might-possibly does, but I couldn't figure it out.

Because of that, I needed to bind the GridView to a DataSource programmatically. And that I did, right off the bat, right when the page loads, because, you know, I want the damn data right the hell away.

So what I neglected to do was put the data binding declarations in the good ol' trusty "if (!Page.IsPostBack)" statement. Which means that each and ever time the page loaded (i.e, when I clicked the "Edit" and "Update" links) the GridView was rebound to the original data. No fracking wonder my stuff wasn't working.

31 March, 2008

Highlighting features with ArcGis Server 9.2

I have the need to highlight items when the user selects to zoom to a particular item from a custom search. If I was using the ResultsTask control, then the user could simply check the checkbox next to the feature, and it would highlight. However, since the client doesn't like the treeview style of the ResultsTask, I have put the search results into an HTML table. Now, when the user selects to zoom to a feature, it will automatically highlight the feature.

I found this code in the ESRI forums, and it works well.

// This gives us a nice light blue color. You can change the color to whatever you want.
ESRI.ArcGIS.ADF.ArcGISServer.RgbColor irgbc = new ESRI.ArcGIS.ADF.ArcGISServer.RgbColor();
irgbc.Red = 50;
irgbc.Green = 255;
irgbc.Blue = 255;
irgbc.AlphaValue = 255;

ESRI.ArcGIS.ADF.ArcGISServer.FIDSet fids = new ESRI.ArcGIS.ADF.ArcGISServer.FIDSet();
int[] ids = new int[1];
ids[0] = Convert.ToInt32(key); //<-- OBJECTID of the feature to highlight
fids.FIDArray = ids;

ESRI.ArcGIS.ADF.ArcGISServer.MapDescription mapdesc = ((MapFunctionality)mf).MapDescription;
ESRI.ArcGIS.ADF.ArcGISServer.LayerDescription[] layerdescs = mapdesc.LayerDescriptions;
ESRI.ArcGIS.ADF.ArcGISServer.LayerDescription layerdesc = layerdescs[layerid];
layerdesc.Visible = true;
layerdesc.SelectionColor = irgbc;
layerdesc.SelectionFeatures = fids.FIDArray;

The problem with the above code is that it highlights the entire area. Not bad for point features, but for polygons, it covers everything underneath it completely. I tried adjusting the RgbColor.AlphaValue, but that didn't change anything at all. I'm thinking that is a red herring, and doesn't really do anything at all. So what I decided to do was instead of filling the entire polygon, I just use ShowSelectBuffer as seen below:

ESRI.ArcGIS.ADF.ArcGISServer.RgbColor irgbc = new ESRI.ArcGIS.ADF.ArcGISServer.RgbColor();
irgbc.Red = 50;
irgbc.Green = 255;
irgbc.Blue = 255;
irgbc.AlphaValue = 255;

ESRI.ArcGIS.ADF.ArcGISServer.FIDSet fids = new ESRI.ArcGIS.ADF.ArcGISServer.FIDSet();
int[] ids = new int[1];
ids[0] = Convert.ToInt32(key);
fids.FIDArray = ids;

ESRI.ArcGIS.ADF.ArcGISServer.MapDescription mapdesc = ((MapFunctionality)mf).MapDescription;
ESRI.ArcGIS.ADF.ArcGISServer.LayerDescription[] layerdescs = mapdesc.LayerDescriptions;
ESRI.ArcGIS.ADF.ArcGISServer.LayerDescription layerdesc = layerdescs[layerid];
layerdesc.Visible = true;
layerdesc.ShowSelectionBuffer = true;
layerdesc.SelectionFeatures = fids.FIDArray;



By using LayerDescription.ShowSelectBuffer, the feature is outlined with the highligh color, instead of filled in. This gives us the ability to see what is within the feature, while still knowing where the feature was located.



References:



http://forums.esri.com/Thread.asp?c=158&f=2276&t=211190&mc=12#msgid719661



http://forums.esri.com/Thread.asp?c=158&f=2276&t=223224&mc=1




28 March, 2008

ESRI breaks my custom zoom to method

I have an ArcGIS Server application. The client didn't like the standard format ESRI uses for displaying search results. So I made my own version. Basically, I return an HTML table through Callback scripting and populate a DIV with it. That table contains a column which is a hyperlink which calls functionality to zoom to that particular feature. Here's my code for that:

 System.Data.DataTable dataTable = null;
ESRI.ArcGIS.ADF.Web.DataSources.IMapFunctionality mf = (ESRI.ArcGIS.ADF.Web.DataSources.IMapFunctionality )Map1.GetFunctionality(0);
if (mf != null)
{
ESRI.ArcGIS.ADF.Web.DataSources.IGISResource gisresource = mf.Resource;
bool supported = gisresource.SupportsFunctionality(typeof(ESRI.ArcGIS.ADF.Web.DataSources.IQueryFunctionality));
int shapeInd = -1;
if (supported)
{
ESRI.ArcGIS.ADF.Web.DataSources.IQueryFunctionality qfunc;
qfunc = (ESRI.ArcGIS.ADF.Web.DataSources.IQueryFunctionality)gisresource.CreateFunctionality(typeof(ESRI.ArcGIS.ADF.Web.DataSources.IQueryFunctionality), null);
string[] lids;
string[] lnames;
qfunc.GetQueryableLayers(null, out lids, out lnames);



All this works splendidly...or at least it did. That was until I went an used ESRI's MapIdentify tool. This tool by default puts things in the task results panel. When I used the zoom to functionality in the task results panel, I noticed that my custom zoom to method suddenly quick functioning. This was distressing to say the least. It seems that every time I actually start to feel like I somewhat understand ArcGIS, I get blind-sided by something stupid like this.



After some careful debugging, I discovered what was happening was that ESRI stacks new MapResourceItems on top of the actual map when you interact with the task results. So originally my code was working fine when I was getting MapResourceItems[0] since that was the only MapResourceItem. But after running the task results, both the layer names and layer ids returned from the GetQueryalbleLayers() method were coming back as GUIDs.



My solution was to loop through all MapResourceItems until I found a MapResourceItem with a name that matches the map name.





  System.Data.DataTable dataTable = null;
ESRI.ArcGIS.ADF.Web.DataSources.IMapFunctionality mf = null;
for (int i = 0; i < this.MapResourceManager1.ResourceItems.Count; i++)
{
if (string.Compare(this.MapResourceManager1.ResourceItems[i].Name, this.m_mapName, true) == 0)
{
mf = (ESRI.ArcGIS.ADF.Web.DataSources.IMapFunctionality)Map1.GetFunctionality(i);
}
}
if (mf != null)
{
ESRI.ArcGIS.ADF.Web.DataSources.IGISResource gisresource = mf.Resource;
bool supported = gisresource.SupportsFunctionality(typeof(ESRI.ArcGIS.ADF.Web.DataSources.IQueryFunctionality));
int shapeInd = -1;
if (supported)
{
ESRI.ArcGIS.ADF.Web.DataSources.IQueryFunctionality qfunc;
qfunc = (ESRI.ArcGIS.ADF.Web.DataSources.IQueryFunctionality)gisresource.CreateFunctionality(typeof(ESRI.ArcGIS.ADF.Web.DataSources.IQueryFunctionality), null);
string[] lids;
string[] lnames;
qfunc.GetQueryableLayers(null, out lids, out lnames);

19 March, 2008

BlogMyCode VS2005 plugin

I have been using Windows Live Writer for quite a while now. I like it. It contains all the info for my different blogs. It is a desktop app. Makes it easy to start a post, save it as a draft, and continue it on later and when you actually get around to posting, it uses current date/time stamp. I know blogger for instance, will post a draft with the date/time stamp of when you started it, now when you finish it.

What I have not been doing is using WLW to its full extent, or even, it seems, a portion of that extent. As I was finally getting it setup on my work computer (I had to bypass the lockout for Windows Update to do it - don't tell the IS folks, ok?). As I was searching for plugins for WLW, I can across a plugin for Visual Studio 2005 which, I have to tell you, Is freaking awesome!

BlogMyCode is Microsoft Visual Studio 2005 plugin, that helps you to blog formatted source code, by using Windows Live Writer.

BlogMyCode adds a "Blog This" element to your context menu in VS2005. Highlight some code, right-click, select "Blog This" and in just a few steps, you get this:

public ParcelIdentify(Map map, string filePath)


{


    m_map = map;


    m_filePath = filePath;


    SetupIdentify();


 


}



The drawback I have found so far (and it is probably a setting somewhere in WLW) is that when I do the "Blog This", it opens a new instance of WLW and starts a new post.



You can get the BlogMyCode VS2005 plugin here.

10 March, 2008

Using SQL Database images in the ASP.NET ReportViewer


One of the criteria for a project was for the user to input data into the database for viewing in a report. The user entered data, and uploaded an image for the report. Now, I've added images to an SQL database before. But years and years ago, way back when there wasn't an 'image' type for a field in SQL. Instead, I believe the type for the field was called 'binary' or something along those lines. The process for inserting an image into an MSSQL image field is a little different than it was before.

I start off with the ASP.NET System.Web.UI.WebControls.FileUpload control for uploading a file through the web page. Back in the ASP days, there wasn't a process for this built into ASP, so we had to 1) buy a DLL for doing this or 2) make a DLL for doing this. Either way, it wasn't integrated, and required either and ActiveX control or a Java applet to get the job done.

The data the user entered was to be available in a report created in ReportViewer. The Reporting Services report has an image object, which has the option for defining it as a database image, and define the field in the database that has the image. Now, the implication is that one would have to define the field and the report would get the data, and display the image. Easy as pie. But the reality is something wholly different. After days on end of searching and searching and reading forum posts and tutorials ad-infinitum, I finally found (although I have to admit, I don't know where now) that it isn't as simple and easy as they let on. You can't just put:

=Fields!ImageField.Value


and have it display an image. No. The report must start with a string for the image, and then the report converts that string to an image. So the question becomes 'how do I get the the data into string format?' Oddly, you have to convert the data to base 64 string, then convert it FROM base 64 string. I don't know why, but I do know this works.

=System.Convert.FromBase64String(System.Convert.ToBase64string(Fields!ImageField.Value))


I don't pretend to know why this is the way it is, but I know that it works, and the image appears. And frankly, since I know how to make it work, I don't really care why I have to do it. Oh, crap, I almost forgot. To use the source Database for a report, you've got to set the MIMEType to either image/jpg, image/png or image/bmp. Again, I don't know why this is, I just know that it is, and that is enough for me.

I spent oodles of time trying to get the image to display, but it just seemed like it would never ever work properly. Finally, I decided to create a quick and dirty (and extremely ugly) desktop application where I would pull the image from the database and assign it to a PictureBox control. What I found was as I would pull the image out of the database, and using a MemoryStream to convert it to an actual image.



System.Data.SqlClient.SqlCommand cmd = new System.Data.SqlClient.SqlCommand();
cmd.CommandText = "SELECT * FROM [REPORT_TABLE]";
System.Data.SqlClient.SqlConnection conn = new System.Data.SqlClient.SqlConnection();
conn.ConnectionString = _connectionString;
cmd.Connection = conn;
System.Data.SqlClient.SqlDataAdapter da = new System.Data.SqlClient.SqlDataAdapter(cmd.CommandText, conn);
DataSet ds = new DataSet();
da.Fill(ds);
DataRow dr = ds.Tables[0].Rows[0];
Byte[] b = (Byte[])dr["IMAGEFIELD"];
MemoryStream ms = new MemoryStream(b);
this.pictureBox1.Image = Image.FromStream(ms);



This code, while it appears on the surface that it should work, didn't. It would kick out the ambiguous "Parameter is not valid" error message at the "Image.FromStream(ms)" line. After several days of googleing and reading what was said by others regarding this error, I discovered that my code was syntactically correct and there was no real reason why it wasn't working. That is when it hit me. I wonder if I'm putting the data into the database correctly?

For inserting the data, at first I was using a stored procedure, through an SqlDataSource and it appeared to be working. That is to say, it didn't kick out any errors. Then I figured I would just go through the process of creating the SqlDataSource and, for lack of a better description, manually insert the data. Again, this seemed to work fine. No errors. But still the image was not visible in the report, and in my desktop application, it would error when trying to create the image "FromStream(Stream stream)". This got me to thinking, so I took a step back and instead of using the SqlDataSource, I decided to try using System.Data.SqlClient to insert the data and image. And lo' and behold this method worked. I don't know what is funky with SqlDataSource method, but what I know is that it didn't work - but that using SqlClient does work, and that is all that really matters to me.


string uploadFileName = string.Empty;
byte[] imageBytes = null;
if(imageUpload != null&& imageUpload.HasFile)
{
uploadFileName = imagUpload.FileName;
imageBytes = imageUpload.FileBytes;
}
System.Data.SqlClient.SqlCommand cmd = new System.Data.SqlClient.SqlCommand();
System.Data.SqlClient.SqlConnection conn = new System.Data.SqlClient.SqlConnection();
conn.ConnectionString = _connectionString;
cmd.CommandText = "INSERT INTO REPORT_TABLE (IMAGEFIELD,IMAGENAME) VALUES (@image,@filename)";
cmd.Parameters.Add("@image", SqlDbType.Image, imageBytes.Length).Value = imageBytes;
cmd.Parameters.Add("@filename", SqlDbType.VarChar, 50).Value = uploadFileName;
cmd.Connection = conn;
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();



Now, just inserting the imageBytes object into the command text did not work. I had to add it as a Parameter to the SqlCommand object. Why? Hell, I don't know, but I know it works, and that is what counts for me at this point. So I put added 23 parameters to the Command object (one for each column of course) and set the values accordingly

cmd.Parameters.Add("@image", SqlDbType.Image, _imageBytes.Length).Value = _imageBytes;


This put the image into the database properly. I'm guessing the parameter is required because it actually defines exactly what DBType we are dealing with, where an normal insert statement wouldn't. At any rate it WORKS! Now the image goes into the database, and get extracted by the ReportViewer and displays properly in the report.

w00t!



Technorati Tags: , , , ,

20 February, 2008

Drill through reports in ASP.NET

In a recent ASP.NET application (coded in C# - natch) I had the cause to create a report, which contained a hyper link which in turn displayed a second report based on data from the first report. In researching this, I discovered this is called a drill through report.

I found information at CodeProject.com regarding drill down reports. The difference between what Shirley did, and what I was doing is that I already had the dataset for the report created. I created a dataset with two data tables in it. I haven't tried it, but I'm guessing that I could have easily put as many tables as I needed if I were going to drill down farther than just one level.

I used the same dataset for both reports. But when I went to perform the drill down, I received the following error:
"A data source instance has not been supplied for the data source [report data source name]"

So I slap my forehead, call my self a dunce, and make sure that both tables are represented by data sources (or would that be data sourcii?). At any rate, I kept getting the same error, which is what led me to googling the error message to see what the problem is. Based on Shirley's article, I realized that I would be required to set the data source for the new report in the ReportViewer_Drillthrough event handler.

Here is my code:

protected void ReportViewer_Drillthrough(object sender, Microsoft.Reporting.WebForms.DrillthroughEventArgs e)
{
Microsoft.Reporting.WebForms.
LocalReport localReport = (Microsoft.Reporting.WebForms.LocalReport)e.Report;
localReport.DataSources.Clear();
this.ObjectDataSource3.SelectParameters[0].DefaultValue = e.Report.GetParameters()[0].Values[0].ToString().Trim();
Microsoft.Reporting.WebForms.
ReportDataSource rds = new Microsoft.Reporting.WebForms.ReportDataSource("ReportDataSet_TABLE",this.ObjectDataSource3.Select());
localReport.DataSources.Add(rds);
}

A bit of breakdown for those who have a hard time following my crappy coding style:
First off, I set a LocalReport object ot the DrillthroughEventArgs.Report object. Then I clear the data sources because I'm going to add a new one, and I certainly don't want the report getting confused. I set the default value of the SelectParameters[0] (the only parameter for this report) to the only parameter that is passed. If there are more parameters in your report, then you would, of course, be required to set them all. For this instance, I had only one.

Then I had to create a new ReportDataSource object. At first I tried simply added the ObjectDataSource3 to my localReport.DataSources, but that didn't fly because I guess an ObjectDataSource is different from a ReportDataSource. Then I figured I would just cast the ObjectDataSource as a ReportDataSource. But that didn't fly either. You can't cast the former as the latter. So I was resigned to create a new one. I gave the new one the same name as the DataSource I defined in my report. Then I added the ReportDataSource to my localReport.DataSources collection, and voila!

Oh, and don't try to refresh the localReport. For some reason it seems to resort back to the parent report. I don't know why, and frankly don't really care, as long as I know how to handle it, it's all good.




Technorati Tags: , , , , ,

04 February, 2008

Working Client-Side with CheckBoxLists

One of the new things I have been playing with are CheckBoxLists. This is a server control which is DataBindable. It creates a list of CheckBox controls. You can define if the list is displayed horizontally, or vertically. I am using a couple of these in a new application and ran into some problems accessing them with JavaScript. This application doesn't ever do a real-honest-to-goodness postback. Instead, all calls to the server are handled through Callbacks. Callbacks aren't really what I'm here to talk about, so if you need to learn more about them, I would recommend popping on over to ASP.NET and checking out their forums and other areas of that site. It is chock full of excellent information.

Ok, back to where I was. Getting to the CheckBoxList stuff. The CheckBoxList renders its ID on the HTML side slightly different from a normal control. Since the control generates multiple sub-controls. For instance, normally, if you had a CheckBox and gave it the ID of "myCheckBox" then to access it in JavaScript you'd simply do something like var chkbx = document.getElementById('<%=myCheckBox.ClientID %>'); and voila! you have access to your CheckBox control. But as stated, the CheckBoxList generates multiple CheckBoxes. The problem was how in the world do I access these? How do I do capture a client side event with a databound list like this? I mean, the list could change daily right? What I came up with (and it probably isn't the _best_ way to go about it, but it certainly works) is to add an attribute to the generated CheckBox in the DataBound EventHandler for the CheckBoxList control. Using DataBound of course because that means the list has been completed.

The CheckBoxList, like the DropDownList (and several other controls) contains the .Items parameter which is the ListItemCollection object. What I ended up doing is adding an "onclick" attribute to each item, passing to the JavaScript the client id of the actual CheckBox control, the text and the value (really, I only needed the control client id and the text, but I figured I'd go ahead and throw in the value for good measure. I separated the ClientID, Text and Value data with a colon (:) so I could easily split it back on the client side.

protected void checkBoxList_DataBound(object sender, EventArgs e)
{
CheckBoxList cbl = (CheckBoxList)sender;
for (int i = 0; i < style="color: rgb(51, 51, 255);">string controlName = cbl.ClientID + "_" + i;
cbl.Items[i].Attributes.Add("onclick", "javascript:checkBoxChanged('" + controlName + ":" + cbl.Items[i].Text + ":" + cbl.Items[i].Value + "')");
cbl.Items[i].Selected = true;
}
}


I use the same DataBound Event Handler for all of the CheckBoxList controls since there is nothing happening that is actually specific to any one CheckBoxList, they all call the same JavaScript. However, getting the control information, and whether or not it is checked, back to the client side is only half the battle. The check boxes are part of a data filter which will display data in a GridView. The user can click on or off all or part of the list items to decrease or expand the scope of the data displayed. So I need to put these checked boxes into some kind of JavaScript array so that when the time comes to do the Callback to populate the GridView, I have all the required data to build the filter server-side.

var checkedBoxesArray = new Array();
function checkBoxChanged(controlInfo) {
var ary = controlInfo.split(":");
var chkBx = document.getElementById(ary[0]);
if(chkBx.checked) {
// check to see if the check box already exists (it shouldn't)
for(x in checkedBoxesArray) {
if(controlInfo == checkedBoxesArray[x]) return; // <-- get out of here, it's already there, and there is nothing else to do
}
checkedBoxesArray[checkedBoxesArray.length+1] = controlInfo;
}else {
for(x in checkedBoxesArray) {
// find the checkbox information in the array and remove it (it should be there)
if (controlInfo == checkedBoxesArray[x]) {
checkedBoxesArray.splice(x,1);
break; // <-- No use going through all of them if we don't have to , right?
}
}
}
}


Then I could access the checkedBoxesArray in the JavaScript function which actually creates the querystring style callback event arguments which are used on the server-side to create the data filter.

07 January, 2008

XP does not support hosting on a UNC share?


It occurs to me that for nearly a decade now Microsoft has been championing the 'thin client.' Saying things like "the network is the OS" and wanting businesses, developers and others to move to heavy-hitting powerful server platforms that would contain most of the programs and information, and the users would have more watered down desktops which would access most of their applications (office, mail, etc) from the server.

So imagine my shock, surprise and stream of words filthy enough to make a sailor blush when I kept getting this error while trying to publish a website:
Error 5 An error occurred loading a configuration file: Failed to start monitoring changes to '\\[machinename]\[drive]$\[directory]\[website]\bin\ko' because the network BIOS command limit has been reached. For more information on this error, please refer to Microsoft knowledge base article 810886. Hosting on a UNC share is not supported for the Windows XP Platform.

WTF? Hosting on a UNC share is not supported? How the hell can this be? Microsoft touts how Visual Studio 2005 is such a great development platform. How collaboration and all that jazz is great and easy and built right into it. What kind of crack are they smoking? If hosting on a UNC is not supported, how in God's green earth are we supposed to develop web applications? On our desktops? Sure, ok, but what about all that great built-in collaboration? Oh, I guess that baby gets thrown out with the bathwater in this one.

What makes things worse is that this seems to be a rather sporadic error. On top of that, I can run the publish command, get this error with a fold like "ko" (as above) and run it again, making no changes, and it gives me a different folder. Seems almost totally random. Thankfully, all the folders it was complaining about were either 1) localization folders added by their AJAX control toolkit, or 2) App_Theme folders that we weren't using anyway. So I was able to delete all those without causing undue issues with the site.

Checking the KB article the error instructs you to (http://support.microsoft.com/?kbid=810886) doesn't really do much for me. First off, the development server we are using didn't have these keys at all in their registry. The KB article is for windows 2000 server, and we are using Windows Server 2k3. So this article is pointless. Every Google find points back to the same damn MS KB article.

AAAAAAAAARRRRRRRGGGGGHHHHHHH!!!!!

I just had to rant a bit.
Sorry.