03 iulie 2009

How a FileZilla bug made me sweat

Happy that I finished some minor task, I was starting to deploy on PR.

Then when I test, surprise, I get the error "Could not load file or assembly 'XXXX' or one of its dependencies. An attempt was made to load a program with an incorrect format.". Pfu. Never got this before, so I started googling. A lot of messages and the conclusion it was that the server is a 64 bit version and I have a 32 bit dll version. But not in my case, both where 32 bit, and the mystery raises more and more.

So I start doing foolish things, like when you do when the PR site is down and customers want to buy things from you. I even uploaded a 64 bit version, I restarted my machine, compiled, uploaded again with AnyCPU, 32, 64 even DEBUG mode. Nothing. Same error.

I've had a fresh build of Windows 7, so I thought this would be the problem. All day I had minor problems with the OS so I was suspecting it too.
Running out of time.

I go remote to the server. I recycle the App Pool. Nothing. Other sites on the server where working fine so I stop doing foolish things on server too.

I used FileZilla ( a version previous 3.2.6.1) to upload the .dll files. While doing this I was seeing that from time to time the upload was interrupting, but in the end was done. A little bit weird, not enough though to get worried, even notice this for good.

But after 45 minutes of reading the same solution on google, trying the same things, even knowing is in vain :), I go the FileZilla to check that upload behaviour. It was happening on FTPS/implicit transfer. I rdc on server again and check the FTP server:

Data connection SSL warning: SSL3 alert write: fatal: bad record mac


Pfu. OK. With the hope that on FTP I won't have any problem, I uploaded again the files. I saw continuous transfer and when I finished, almost certain it will work the site, I refreshed the browser and voila, all working good again.

This is the kind of situation which really makes you to like to smoke a cigar watching the night on the 5th floor. Almost like after sex.

The bug was reported here

27 mai 2009

ASP .Net server control for youtube embeded videos

Here is ASP .Net server control for youtube embeded videos.
The param definitions can be found here
Only Src param is required, other parameters are generated only if have another value than the default one.
Usage

<cc1:YouTubeEmbed ID="YouTubeEmbed1" runat="server" Src="http://www.youtube.com/v/P38oxTrWYp0&f=videos&c=ytapi-AdrianIftode-youtubelyricsmas-ia9d2g0e-0"
AutoPlay="false"
AllowFullScreen="false"
Border="true"
PrimaryBorderColor="0x6b8ab6"
SecondayBorderColor="0x2b405b"
GenieMenu="true"
HighDefinitionPlayback="false"
LoadRelatedVideos="false"
Loop="false"
ShowAnnotations="true"
ShowClosedAnnotations="true"
ShowInfo="false"
ShowSearch="false"
Start="68"
Width="600"
Height="400"/>

It creates the html for the following video.


Source code

using System.Web.UI.WebControls;
using System.Web.UI;
using System.ComponentModel;
using System;
using System.Text;

namespace YouTubeControls
{
[DefaultProperty("Src"),
ToolboxData("<{0}:YouTubeEmbed runat=server></{0}:YouTubeEmbed>")]

public class YouTubeEmbed : WebControl
{
private string src;

[Bindable(true),
Category("Data"),
Description("Video source.")
]

public string Src {
get {
return src;
}
set {
src = value;
}
}

[PersistenceMode(PersistenceMode.Attribute),
Bindable(true),
Category("Appearence"),
DefaultValue(425)]
public int Width
{
get
{
var obj = ViewState["Width"];
return (obj == null) ? 425 : ((int)obj > 0) ? (int)obj : 425 ;
}
set
{
ViewState["Width"] = value;
}
}
[PersistenceMode(PersistenceMode.Attribute),
Bindable(true),
Category("Appearence"),
DefaultValue(349)]
public int Height
{
get
{
var obj = ViewState["Height"];
return (obj == null) ? 349 : ((int)obj > 0) ? (int)obj : 349 ;
}
set
{
ViewState["Height"] = value;
}
}

[Bindable(true),
Category("Behavior"),
DefaultValue(true),
Description("Sets whether the player should load related videos."),
PersistenceMode(PersistenceMode.Attribute)
]
public bool LoadRelatedVideos
{
get
{
var obj = ViewState["Rel"];
return (obj == null) ? true: (bool)obj;
}
set
{
ViewState["Rel"] = value;
}
}

[Bindable(true),
Category("Behavior"),
DefaultValue(false),
Description("Sets whether or not the initial video will autoplay when the player loads."),
PersistenceMode(PersistenceMode.Attribute)]
public bool AutoPlay
{
get
{
var obj = ViewState["AutoPlay"];
return (obj == null) ? false : (bool)obj;
}
set
{
ViewState["AutoPlay"] = value;
}
}
[Bindable(true),
Category("Behavior"),
DefaultValue(false),
Description("Cause the player to play the initial video again and again."),
PersistenceMode(PersistenceMode.Attribute)]
public bool Loop
{
get
{
var obj = ViewState["Loop"];
return (obj == null) ? false : (bool)obj;
}
set
{
ViewState["Loop"] = value;
}
}
[Bindable(true),
Category("Behavior"),
DefaultValue(false),
Description("Sets whether or not enable Javascript API."),
PersistenceMode(PersistenceMode.Attribute)]
public bool EnabledJSApi
{
get
{
var obj = ViewState["EnabledJSApi"];
return (obj == null) ? false : (bool)obj;
}
set
{
ViewState["EnabledJSApi"] = value;
}
}

[Bindable(false),
Category("Behavior"),
Description("Value can be any alphanumeric string. This setting is used in conjunction with the JavaScript API."),
PersistenceMode(PersistenceMode.Attribute)]
public string PlayerApiID
{
get
{
var obj = ViewState["PlayerApiID"];
return (obj == null) ? null : (string)obj;
}
set
{
ViewState["PlayerApiID"] = value;
}
}

[Bindable(true),
Category("Behavior"),
DefaultValue(false),
Description("Disable the player keyboard controls."),
PersistenceMode(PersistenceMode.Attribute)]
public bool DisableKeyboard
{
get
{
var obj = ViewState["DisableKeyboard"];
return (obj == null) ? false : (bool)obj;
}
set
{
ViewState["DisableKeyboard"] = value;
}
}
[Bindable(true),
Category("Appearence"),
DefaultValue(false),
Description("Genie menu (if present) appears when the user's mouse enters the video display area, as opposed to only appearing when the menu button is pressed."),
PersistenceMode(PersistenceMode.Attribute)]
public bool GenieMenu
{
get
{
var obj = ViewState["GenieMenu"];
return (obj == null) ? false : (bool)obj;
}
set
{
ViewState["GenieMenu"] = value;
}
}
[Bindable(false),
Category("Behaviour"),
DefaultValue(false),
Description("Enables a border around the entire video player."),
PersistenceMode(PersistenceMode.Attribute)]
public bool Border
{
get
{
var obj = ViewState["Border"];
return (obj == null) ? false : (bool)obj;
}
set
{
ViewState["Border"] = value;
}
}
[Bindable(false),
Category("Appearence"),
Description("Primary border color. Any RGB value in hexadecimal format"),
PersistenceMode(PersistenceMode.Attribute)]
public string PrimaryBorderColor
{
get
{
var obj = ViewState["PrimaryBorderColor"];
return (obj == null) ? null : (string)obj;
}
set
{
ViewState["PrimaryBorderColor"] = value;
}
}
[Bindable(false),
Category("Appearence"),
Description("Secondary border color. Any RGB value in hexadecimal format"),
PersistenceMode(PersistenceMode.Attribute)]
public string SecondayBorderColor
{
get
{
var obj = ViewState["SecondayBorderColor"];
return (obj == null) ? null : (string)obj;
}
set
{
ViewState["SecondayBorderColor"] = value;
}
}
[Bindable(false),
Category("Behaviour"),
DefaultValue(0),
Description("This parameter causes the player to begin playing the video at the given number of seconds from the start of the video."),
PersistenceMode(PersistenceMode.Attribute)]
public int Start
{
get
{
var obj = ViewState["Start"];
return (obj == null) ? 0 : ((int)obj > 0) ? (int)obj : 0;
}
set
{
ViewState["Start"] = value;
}
}
[Bindable(true),
Category("Behavior"),
DefaultValue(false),
Description("Enables the fullscreen button."),
PersistenceMode(PersistenceMode.Attribute)]
public bool AllowFullScreen
{
get
{
var obj = ViewState["AllowFullScreen"];
return (obj == null) ? false : (bool)obj;
}
set
{
ViewState["AllowFullScreen"] = value;
}
}
[Bindable(true),
Category("Behavior"),
DefaultValue(false),
Description("Enables HD playback."),
PersistenceMode(PersistenceMode.Attribute)]
public bool HighDefinitionPlayback
{
get
{
var obj = ViewState["HighDefinitionPlayback"];
return (obj == null) ? false : (bool)obj;
}
set
{
ViewState["HighDefinitionPlayback"] = value;
}
}
[Bindable(true),
Category("Behavior"),
DefaultValue(true),
Description("Enables HD playback."),
PersistenceMode(PersistenceMode.Attribute)]
public bool ShowSearch
{
get
{
var obj = ViewState["ShowSearch"];
return (obj == null) ? true : (bool)obj;
}
set
{
ViewState["ShowSearch"] = value;
}
}
[Bindable(true),
Category("Behavior"),
DefaultValue(true),
Description("Display information like the video title and rating before the video starts playing."),
PersistenceMode(PersistenceMode.Attribute)]
public bool ShowInfo
{
get
{
var obj = ViewState["ShowInfo"];
return (obj == null) ? true : (bool)obj;
}
set
{
ViewState["ShowInfo"] = value;
}
}
[Bindable(true),
Category("Behavior"),
DefaultValue(false),
Description("Display annotations."),
PersistenceMode(PersistenceMode.Attribute)]
public bool ShowAnnotations
{
get
{
var obj = ViewState["ShowAnnotations"];
return (obj == null) ? false : (bool)obj;
}
set
{
ViewState["ShowAnnotations"] = value;
}
}
[Bindable(true),
Category("Behavior"),
DefaultValue(false),
Description("Display closed annotations."),
PersistenceMode(PersistenceMode.Attribute)]
public bool ShowClosedAnnotations
{
get
{
var obj = ViewState["ShowClosedAnnotations"];
return (obj == null) ? false : (bool)obj;
}
set
{
ViewState["ShowClosedAnnotations"] = value;
}
}
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
Page.RegisterRequiresControlState(this);
}
protected override object SaveControlState()
{

object obj = base.SaveControlState();

if (!String.IsNullOrEmpty(src))
{
if (obj != null)
{
return new Pair(obj, src);
}
else
{
return src;
}
}
else
{
return obj;
}
}
protected override void LoadControlState(object state)
{
if (state != null)
{
Pair p = state as Pair;
if (p != null)
{
base.LoadControlState(p.First);
src = (string)p.Second;
}
else
{
if (state is string)
{
src = (string)state;
}
else
{
base.LoadControlState(state);
}
}
}
}


protected override void RenderContents(HtmlTextWriter writer)
{
var sb = new StringBuilder();
sb.AppendFormat("<object width=\"{0}\" height=\"{1}\">", Width, Height);
sb.AppendLine();
var sbMovieParam = new StringBuilder();
sbMovieParam.AppendFormat("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}{16}",
Src,
AutoPlay ? "&autoplay=1" : "",
Loop ? "&loop=1" : "",
EnabledJSApi ? "&enablejsapi=1" : "",
!String.IsNullOrEmpty(PlayerApiID) ? "&playerapiid="+ PlayerApiID : "",
DisableKeyboard ? "&disablekb=1" : "",
GenieMenu ? "&egm=1" : "",
(Border && (!String.IsNullOrEmpty(PrimaryBorderColor) || !String.IsNullOrEmpty(SecondayBorderColor))) ? "&border=1" : "",
(!String.IsNullOrEmpty(PrimaryBorderColor) && Border) ? "&color1="+ PrimaryBorderColor : "",
(!String.IsNullOrEmpty(SecondayBorderColor) && Border) ? "&color2="+ SecondayBorderColor : "",
Start > 0 ? "&start="+Start : "",
AllowFullScreen ? "&fs=1" : "",
HighDefinitionPlayback ? "&hd=1" : "",
ShowSearch == false ? "&showsearch=0" : "",
ShowInfo == false ? "&showinfo=0" : "",
ShowAnnotations == false ? "&iv_load_policy=3" : "",
ShowClosedAnnotations ? "&cc_load_policy=1" : ""
);
var movieParam = sbMovieParam.ToString();
sb.AppendFormat("<param name=\"movie\" value=\"{0}\"></param>", movieParam);
if ( AllowFullScreen ){
sb.AppendLine("<param name=\"allowFullScreen\" value=\"true\"></param>");
}
else{
sb.AppendLine("<param name=\"allowFullScreen\" value=\"false\"></param>");
}
sb.AppendLine();
sb.AppendFormat("<embed src=\"{0}\" type=\"application/x-shockwave-flash\" width=\"{1}\" height=\"{2}\"{3}></embed>"
,movieParam, Width, Height,
AllowFullScreen ? " allowfullscreen=\"true\"" : " allowfullscreen=\"false\""
);
sb.AppendLine("</object>");
var embed = sb.ToString();
writer.Write(embed);
}
}
}


Download library
8 Jun 2008 Update: link fixed

14 mai 2009

Think LINQ deffered execution like an non-clustered SQL View

LINQ deferred execution is when you create the query, but this is not executed until you need it. So first you tell what you need and you'll get it when is needed by calling methods like ToList, First, Single, etc.


var ctx = ...
var query = from p in ctx.products
where p.enabled == true && c.deleted == false
select p;


After calling query.ToList(), the LINQ creates the SQL, sends it to server, get the results and then creates the products list. query is an IQueryable<T> object and ToList creates an IList<T> object.

An non-clustered SQL View acts pretty same like the LINQ non deferred execution. First you define the query, then you call it or use it in any other queries.

CREATE VIEW vwProducts
AS
SELECT p.*
FROM dbo.products p
WHERE p.enabled = 1 AND p.deleted = 0


SELECT *
FROM vwProducts p
WHERE p.description like '%potatoes%'

SQL Server will expand the View vwProducts when the last query is executed.

So, how to see the first LINQ query like a view?

First thing needed is somebody to define/create it.

private IQueryable<product> createProductsView(DBContext ctx){
var query = from c in ctx.products
where p.enabled == true && c.deleted == false
select p;
return query; // recall this now the query isn't yet executed
}

Next I can use in other methods, probably in the ones used by UI

public IList<product> FindByKeyword(string keyword){
var ctx = ContextFactory.CreateContext();
var query = createProductsView(ctx);

query = from p in query // isn't executed yet the query
where p.description.Contains(keyword)
select p;

var productsList = query.ToList(); // it is now
return productsList;
}


Often views are used to simplify database queries. Imagine you can do this at a higher level.

24 aprilie 2009

CLR custom aggregates for SQL Server

Having the tables "product" and "product_stock" I want to display for each product the comma-separated list of sizes and this list is ordered by a specific order.

CREATE TABLE product(
product_id int,
code varchar(50) )

CREATE TABLE product_stock(
product_stock_id int,
size_order int ,
size varchar(20) ,
product_id int )

-- desired output
Product_id Sizes In Stock
35 UK7, UK8, UK9, UK11, UK10.5, UK12, UK10
36 L, M, S, XL, XXL, XXXL


In SQL this can be achieved with cursors, but everyone suggest not to use them. After some digging I found out about custom aggregates and they saved the day.

In the MSDN article is an example which concatenates the strings and this I'm writing does the same, only it adds the ordering feature.

The problem occurs with the ordering because SQL doesn't allow to use the "ORDER BY" clause in subqueries or if the "GROUP BY" exists, then the only columns allowed are the ones in grouping or in the aggregate functions. Raises the following errors:
Msg 1033, Level 15, State 1, Line 9
The ORDER BY clause is invalid in views, inline functions, derived tables, subqueries, and common table expressions, unless TOP or FOR XML is also specified.

and
Msg 8127, Level 16, State 1, Line 4
Column "product_stock.size_order" is invalid in the ORDER BY clause because it is not contained in either an aggregate function or the GROUP BY clause.


It can be used "SELECT TOP 100 PERCENT" to get rid of the annoying message, but is not guaranteed that the order is preserved.

So the following code will solve the problem.

using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.IO;
using System.Text;
using Microsoft.SqlServer.Server;


[Serializable]
[SqlUserDefinedAggregate(
Format.UserDefined, //custom serialization
IsInvariantToNulls = true,
IsInvariantToDuplicates = false,
IsInvariantToOrder = false, //MSDN: Reserved for future use. This property is not currently used by the query processor: order is currently not guaranteed.
MaxByteSize = 8000)
]
public class ConcatenateSizes : IBinarySerialize
{
[Serializable]
public class Size
{
public int SizeOrder { get; set; }
public string SizeName { get; set; }
}
private List sizes = null;
public void Init()
{
sizes = new List();

}
public void Accumulate(SqlString sizeName)
{
if (sizeName.IsNull)
return;

var trims = sizeName.Value.Split(',');
sizes.Add(new Size
{
SizeOrder = Int32.Parse(trims[0]),
SizeName = trims[1]
});
}
public void Merge(ConcatenateSizes part)
{
sizes.AddRange(part.sizes);
}

public SqlString Terminate()
{
sizes.Sort(new SizeComparer());
var sb = new StringBuilder();
if (sizes.Count > 0)
{
foreach (var s in sizes)
sb.Append(s.SizeName).Append(", ");
var result = sb.ToString().TrimEnd(',', ' ');
return new SqlString(result);
}
return null;
}
public void Read(BinaryReader reader)
{
sizes = new List();
try
{
while (true)
{
var size = new Size();
size.SizeName = reader.ReadString();
size.SizeOrder = reader.ReadInt32();
if ( !String.IsNullOrEmpty(size.SizeName))
sizes.Add(size);
else
{
break;
}
}

}
catch (EndOfStreamException)
{
}

}
public void Write(BinaryWriter writer)
{
foreach (var size in sizes)
{
writer.Write(size.SizeName);
writer.Write(size.SizeOrder);
}
}

public class SizeComparer : IComparer
{
#region IComparer Members

public int Compare(Size x, Size y)
{
if (x.SizeOrder != y.SizeOrder)
return x.SizeOrder.CompareTo(y.SizeOrder);
return x.SizeName.CompareTo(y.SizeName);
}

#endregion
}

}


The function is used like:


select p.code, s.sizes
from product p
inner join
(
select ps.product_id, dbo.ConcatenateSizes(convert(varchar(20), ps.size_order) + ',' + ps.size) as sizes
from product_stock ps
group by ps.product_id
) s
on p.product_id = s.product_id

13 aprilie 2009

Tracking codes in DEBUG-RELEASE mode

While you work on a website which includes some JavaScript for counters and statistics is better not to allow or to "hide" them during the development time.

One solution is to comment the source code and every time before the page is published, to uncomment the relevant lines. This leads to errors sooner or later, we use to forget things (uncomment things).

A better option is to use preprocessor directives #if, #endif with the DEBUG compile option.
A protected(at least) boolean field must be added in the code behind.


public partial class home : System.Web.UI.Page
{

#if (DEBUG)
protected static readonly bool Debug = true;
#else
protected static readonly bool Debug = false;
#endif
}



Now the field Debug can be used in the web form. The following google analytics code will exist only when the source code is compiled in Release mode.

<%if (!Debug) { %>
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>

<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("UA-XXXXX-1");
pageTracker._trackPageview();
} catch (err) { }
</script>
<%}%>

01 aprilie 2009

Levenshtein distance - use on web site search

Levenshtein distance is the minimum number of character deletion (D), insertion (I) or substitution(S) operations to transform a string to another.

I used this algorithm to give alternatives to an user when he gets no results after he submitted search by a specific keyword. Often happens when the keyword is misspelled or even there are no matches.

If an e-shop sells very expensive bracelets and has one called "Diamond Tennis Bracelet", it would be nice when the customer searched for "tenis" and he found nothing to give him a close suggestion like "tennis" and no random ones.

Here follows the Levenshtein algorithm implementation in C# in a O(mn) space

 public static int distanceMN(string source, string destination, bool ignoreCase, int threshold) {
if (ignoreCase)
{
source = source.ToLower();
destination = destination.ToLower();
}
if (source == destination)
return 0;
var m = destination.Length;
var n = source.Length;

if (System.Math.Abs(m - n) > threshold)
{
return Int32.MaxValue;
}

var d = new int[m + 1, n + 1];
for (int i = 0; i <= m; i++)
d[i, 0] = i;
for (int j = 0; j <= n; j++)
d[0, j] = j;

for (int i = 1; i <= m; i++)
{

for (int j = 1; j <= n; j++)
{
var cost = destination[i - 1] == source[j - 1] ? 0 : 1;
var insertion = d[i, j - 1] + 1;
var substitution = d[i - 1, j - 1] + cost;
var deletion = d[i - 1, j] + 1;
d[i, j] = minimum(deletion, insertion, substitution);
}
}
return d[m, n];
}


Threshold parameter is used when want to have an upper bound limit for the distance. Is useful when the difference between words will be high for sure.

Based on wikipedia, I rewrote the algorithm for an O(m) space, by using a matrix with 2 lines.


public static int distance2M(string source, string destination, bool ignoreCase, int threshold)
{
if (ignoreCase)
{
source = source.ToLower();
destination = destination.ToLower();
}
if (source == destination)
return 0;
var m = destination.Length;
var n = source.Length;

if (System.Math.Abs(m - n) > threshold) {
return Int32.MaxValue;
}

var d = new int[2, n + 1];

d[0, 0] = 0;
d[1, 0] = 1;

for (int j = 0; j <= n; j++)
d[0, j] = j;

for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
var cost = destination[i-1] == source[j-1] ? 0 : 1;
var insertion = d[1, j - 1] + 1;
var substitution = d[0, j - 1] + cost;
var deletion = d[0, j] + 1;
d[1, j] = minimum(deletion, insertion, substitution);
}
// move the new values on the first line
d[0, 0] = i;
d[0, 1] = i+1;
for (int j = 1; j <= n; j++) {
d[0, j] = d[1,j];
}
}

return d[1, n];
}

Can try this on the website urbanindustry.co.uk by searching "addidas" or anything weird.

30 martie 2009

Eval: Sometimes I like spagheti code

I just discovered some interesting features of the construct <%# Eval() %>. Eval function uses reflection to evaluate a property of a bound object.

I have two classes Product and Category and I want to display the available products in a page, by using a data bound control.

public class Product {
public int ID;
public string Name;
public int Stock;
public DateTime Date;
public Category Category;
}
public class Category {
public string Name;
public DateTime Date;
}


In a data template ( ItemTemplate for sample ) I can use the Eval function like:

1. very simple evaluation without using formats
<%# Eval("Name") %> - will output the name
<%# Eval("Stock") %> - will output the stock ( ..and so on )

2. using a format
<%# Eval("Date","{0:yyyy MMM dd}") %> - for sample: 2009 Apr 06
<%# Eval("Stock","{0} items") %> - will output "10 items" if the stock is 10

3. Combining two or more properties:
<%# "The " + Eval("Name") + " has " + Eval("Stock") + " items in stock." %>
notice that the operators "+" are in the construction <%# %> and the expression is not a concatenation between different constructs

4. Can use more dots!
<%# Eval("Category.Name") %>

5. Can use casting
<%# (int)Eval("Stock") %>

6. What about if you need to compare things
<%# ((int)Eval("Stock") >= 1) ? ("<a href='products.aspx?id=" + Eval("ID") + "'>Buy now!!!</a>") : "Not in stock!" %>

7. Are you aware of a null exception ?
<%# (Eval("Category") != null) ? Eval("Category.Name") : "No category" %>

All can be rewritten in the code behind, on ItemDataBound event. Sometimes I'm too lazy or bored to add a handler and I prefer to write such kind of Eval expressions. The disadvantages can be the heavy use of reflection or the spaghetti code. A plus is you don't need recompile the things as you would be needed when you treat it on ItemDataBound event.

21 martie 2009

Using UrlRewritingNet and Elmah together



 

UrlRewritingNet is an Url rewriting tool for ASP .Net and Elmah is a module for logging unhandled errors.

 


UrlRewritingNet can set a default location for “directory” requests via defaultPage property in <urlrewritingnet> section. When a file without extension is requested the defaultPage value is appended to the original URL. 

 

Elmah provides a handler for getting the errors summary, usually called elmah.axd. This handler also responds to the followings requests: 


 

/elmah.axd/rss – RSS errors list feed 


/elmah.axd/digestrss – RSS digest 


/elmah.axd/download –  comma separated errors list


/elmah.axd/about – about page 


/elmah.axd/stylesheet – the stylesheet used


/elmah.axd/detail?id=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX – html single error summary 


/elmah.axd/xml?id=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX – xml single  error summary


/elmah.axd/json?id=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX – json single error summary 


 


These requests are like a request for a file with no extension. This is why the  UrlRewritingNet adds the defaultPage value which leads to the 404 Http response. First sign of this behavior is when the generated  html does not contain any CSS style applied.

 


To fix this situation I simply removed the defaultPage attribute from <urlrewritingnet>.

 


Now if the default page is not set on IIS, then will get an error when the web site is accessed only by the domain name. This situation was handled by  UrlRewritingNet, but in not a proper way, because it returns a 302 Http response(302 Found) with the location set by the defaultPage attribute value, and I think is not the best solution to give the first page when is about SEO. The drawback when the defaultPage attribute is removed is when the directories in the web site are accessed, it will give a 403.14 – Forbidden(IIS 7). But this can be handled by UrlRewritingNet using a custom UrlRewritingProvider.


During developing this custom provider I noticed it would be better to set and the HttpStatus when the url's are rewriten. So I added a new attribute to the <add/> node called httpStatusCode.


Creating a new UrlRewriterNet is very simple:


public class RootProvider : UrlRewritingProvider

{

    public override RewriteRule CreateRewriteRule()

    {

        return new RootRule();

    }

    }

The class RootRule does all the logic:

 public class RootRule : RewriteRule

    {

    public override void Initialize(UrlRewritingNet.Configuration.RewriteSettings rewriteSettings)

    {

        base.Initialize(rewriteSettings);

        VirtualUrl = rewriteSettings.GetAttribute("virtualUrl", "");

        DestinationUrl = rewriteSettings.GetAttribute("destinationUrl", "");

        HttpStatusCode = rewriteSettings.GetAttribute("httpStatusCode", "");

 

    }

    public override bool IsRewrite(string requestUrl)

    {      

        return requestUrl == VirtualUrl;

    }

    public override string RewriteUrl(string url)        

    {

        if (!String.IsNullOrEmpty(HttpStatusCode))

        {

        HttpStatusCodeHandler handler = null;

        switch (HttpStatusCode)

        {

            case "404" :

            handler = new _404Handler(DestinationUrl);

            break;

            case "301":

            handler = new _301Handler(DestinationUrl);

            break;

            case "302":

            handler = new _302Handler(DestinationUrl);

            break;

            default:

            handler = new NotImplementedHandler(DestinationUrl);

            break;

        }

        handler.Execute();

        return null;

        }

       

        return DestinationUrl;

    }

    public string VirtualUrl { get; set; }

    public string DestinationUrl { get; set; }

    public string HttpStatusCode { get; set; }

    }

The  RootRule class instantiates a specific handler, depending by the http status code.

I created a base class to define the model of how a status code could be handled.

public class HttpStatusCodeHandler {

    protected string destinationUrl;

    protected HttpStatusCodeHandler() { }

    public HttpStatusCodeHandler(string DestinationUrl) {

        destinationUrl = DestinationUrl;

    }        

    public virtual void Execute() {

        throw new NotImplementedHttpStatusException();

    }

    }

 

For sample when a directory is accessed it can be used a 404 response. 

 

    public sealed class _404Handler : HttpStatusCodeHandler

    {

    private _404Handler() { }

    public _404Handler(string DestinationUrl) : base(DestinationUrl) { }

    public override void Execute()

    {

        HttpContext.Current.Response.Clear();

        HttpContext.Current.Response.Status = "404 Not Found";

        HttpContext.Current.Response.StatusDescription = "Not Found";

        HttpContext.Current.Response.Write(File.ReadAllText(HttpContext.Current.Server.MapPath(destinationUrl)));

        HttpContext.Current.Response.End();

    }

    }

 

The 302 and 301 reponses needs to add the Location in the header response. The location contains the new URL where the old resource exists now. 

 

    public sealed class _302Handler : HttpStatusCodeHandler

    {

    private _302Handler() { }

    public _302Handler(string DestinationUrl) : base(DestinationUrl) { }

    public override void Execute()

    {

        HttpContext.Current.Response.Clear();

        HttpContext.Current.Response.Status = "302 Found";

        HttpContext.Current.Response.StatusDescription = "Found";

        HttpContext.Current.Response.AddHeader("Location", destinationUrl);

        HttpContext.Current.Response.End();

    }

    }

    public sealed class _301Handler : HttpStatusCodeHandler

    {

    private _301Handler() { }

    public _301Handler(string DestinationUrl) : base(DestinationUrl) { }

    public override void Execute()

    {

        HttpContext.Current.Response.Clear();

        HttpContext.Current.Response.Status = "301 Moved Permanently";

        HttpContext.Current.Response.StatusDescription = "Moved Permanently";

        HttpContext.Current.Response.AddHeader("Location", destinationUrl);

        HttpContext.Current.Response.End();

    }

    }

    public sealed class NotImplementedHandler : HttpStatusCodeHandler

    {

    private NotImplementedHandler() { }

    public NotImplementedHandler(string DestinationUrl) : base(DestinationUrl) { }

    public override void Execute()

    {

        throw new NotImplementedHttpStatusException();

    }

    }

    public class NotImplementedHttpStatusException : Exception

    {

    public override string Message

    {

        get

        {

        return "NotIplementedHttpStatusException";

        }

    }

    }

Now in the RewriteRule section I define some rules:

to define a default page when the folder “products” is accessed

<add name="products" virtualUrl="/products/" destinationUrl="/products/latest_products.asp" httpStatusCode="302" rewriteUrlParameter="ExcludeFromClientQueryString" ignoreCase="true" provider="RootProvider"/>    

to say that a page is permanently moved and the new location is other page

<add name="about" virtualUrl="/pages.asp?func=get_content&amp;page_id=1" destinationUrl="/about.asp" httpStatusCode="301" rewriteUrlParameter="IncludeQueryStringForRewrite" ignoreCase="true" provider="RootProvider"/>

 

These redirects are very helpful when you want to keep the search engines rankings. 

 

 

 

 

24 februarie 2009

Un required validator pentru DropDownList cu RangeValidator

Validatorul RangeValidator va aparea in UI ca un RequiredValidator. Utilizatorul nu va putea trece mai departe daca nu alege o optiune de la 1 la 5. :)

<asp:DropDownList ID="ddlTitle" runat="server">
<asp:ListItem Value="0">Select Title</asp:ListItem>
<asp:ListItem Value="1">Mr</asp:ListItem>
<asp:ListItem Value="2">Mrs</asp:ListItem>
<asp:ListItem Value="3">Miss</asp:ListItem>
<asp:ListItem Value="4">Ms</asp:ListItem>
<asp:ListItem Value="5">Dr</asp:ListItem>
</asp:DropDownList>

<asp:RangeValidator ID="rangeValidator"
Text="*"
ErrorMessage="The title is required."
ControlToValidate="ddlTitle"
Type="Integer"
MinimumValue="1"
MaximumValue="5"
runat="server"
></asp:RangeValidator>

28 octombrie 2008

JQuery si WebMethod

Prin noiembrie 2006 descoperisem la HALO interactive posibilitatea de a apela dintr-o pagina HTML o metoda dintr-o pagina aspx folosind un ScriptManager. Trebuia sa fac o autentificare in doi pasi. ScriptManger-ul genereaza ceva JavaScript folosit pentru a construi apelul AJAX, dar si pentru a folosi o sintaxa de genul PageMethods.MethodName, valabila atata timp cat e setata true optiunea EnablePageMethods.
Mult mai usor de scris e insa atunci cand se foloseste JQuery. O dezbatere am gasit aici.
Vreau doar sa dau un exemplu intersant de folosit intr-un shopping cart. Intr-un astfel de site exista cred mai multe pagini statice, decat dinamice. Adica fizic mai multe html-uri decat aspx-uri. Acum pe fiecare pagina html sau aspx apare undeva in pagina numarul de produse puse in cos. Daca ar fi sa se aleaga Master Pages pentru a-l afisa, odata ce se adauga o pagina noua in site trebuie recompilat tot situl. Solutia cea mai buna e sa se foloseasca AJAX. Partea frumoasa e ca poti face cumparaturi pe o pagina ASPX, iar cand ajungi in pagina HTML nu trebuie sa iti faci griji ca a disparut cosul, pentru ca AJAX tine cont de sesiune.

Pe server metoda trebuie sa se afle intr-o pagina ASPX, sa fie publica, statica si decorata cu atributul WebMethod;

[WebMethod]
public static int GetItemsCount()
{
if (HttpContext.Current.Session["items"] != null
&& HttpContext.Current.Session["items"] is Int32)
return ((Int32)HttpContext.Current.Session["items"]);
return 0;
}


La nivel de client (browser), se face apelul folosind JQuery. Nici o alta setare magica nu se face ( cu exceptia faptului ca trebuie download fisierul JavaScript care contine JQuery ).


$(document).ready(
$.ajax({
type: "POST",
url: "Products.aspx/GetItemsCount",
data: "{}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(msg) {

$("#productsCount").text(msg.d);
}
})
);


De fiecare data, la incarcarea paginii pe client, se va face un apel la la metoda GetItemsCount. Rezultatul este pus in interiorul unui element din html care are idul productsCount.

Codul asta l-am pus intr-un fisier js si poate fi referentiat intr-o pagina HTML.


<head>
<title>Help</title>

<script src="js/jquery.js" type="text/javascript"></script>

<script src="js/shopping cart.js" type="text/javascript"></script>

</head>


Acu.. Download source code and play

08 octombrie 2008

Conversie data pentru cultura romana (ro-RO)

Vreau sa convertesc date calendaristice din cultura romana (ro-RO) din string in DateTime. Datele sunt de genul 8 Oct 2008, 9 Oct 2008, 12 Dec 2008. DateTime.TryParseExact accepta formatul "d MMM yyyy", numai ca pentru cultura romana datele trebuie sa fie de forma 8 Oct. 2008, 9 Oct. 2008, 12 Dec. 2008 (cu punct dupa luna). Obtin expresia regulata pentru gasirea lunii din data, iar dupa luna inserez punctul.
Pentru asta am nevoie de un MatchEvaluator care e un delegat cu signatura delegate string MatchEvaluator(Match m)

static string Evaluator(Match match) {
return match.ToString() + ".";
}

Evolutia pointerilor la functii incepand de la C# 1.0 pana la C# 3.0 e una pozitiva, in sensul claritatii codului. Daca in 1.0 trebuia sa scrii efectiv metoda ca membru al unei clase, iar in 2.0 nu e tocmai naturala inserararea codului printre parametri, in 3.0 expresiile lambda simplifica mult tehnica.



var strDate = "8 Oct 2008";
var regForReplace = new Regex(@"[a-zA-Z]{3}");

//C# 1.1 delegat
var sDate = regForReplace.Replace(strDate, Evaluator);
Console.WriteLine(sDate); // 8 Oct. 2008

//C# 2.0 metoda anonima
var aDate = regForReplace.Replace(strDate, delegate (Match match ){
return m.ToString() + ".";
});
Console.WriteLine(aDate); // 8 Oct. 2008


//C# 3.0 expresie lambda
var lDate = regForReplace.Replace(strDate, match => match.ToString() + ".");
Console.WriteLine(lDate); // 8 Oct. 2008


DateTime date;
if (DateTime.TryParseExact(lDate , "d MMM yyyy",
CultureInfo.GetCultureInfo("ro-RO"),
DateTimeStyles.None, out date))
{
Console.WriteLine(date); //08.10.2008 00:00:00
}

25 septembrie 2008

null == null ?

Test:

T - SQL:

if ( null = null )
print 'null == null'
else
print 'null != null'

C#:

Console.WriteLine(null == null);


Ce afiseaza fiecare? Si de ce?

03 septembrie 2008

O fi fost vreun bug in Google Chrome?

Ca si istoric, am vrut sa inspectez un element img cu imaginea inexistenta ( adica lipsea de pe server)



Spre deosebire de FireFox, nu imi da optiunea de a restaura sesiunea dinaintea erorii.

08 august 2008

Despre CompositeDataBoundControl

CompositeDataBoundControl este clasa de baza pentru DetailsView, FormView si GridView si are o metoda abstracta ce trebuie implementata de orice clasa mostenitoare.

protected abstract int CreateChildControls(IEnumerable dataSource, bool dataBinding);

dataBinding reprezinta:
-true: controlul va fi construit pe baza unui dataSource
-false: controlul se va reconstrui din ViewState

DetailsView, FormView sau GridView ofera diverse templaturi: de edit, de view sau atunci cand nu exista date in dataSource poti sa arati un anumit mesaj ce prezinta aceasta stare. Ca idee un template face legatura intre date si HTMLul scris de developer la design time, adica separa prezentarea de implementare.

Din urmatorul Xml, care reprezinta organizarea unei anumite firme, as vrea sa obtin un control care sa arate ierarhia din acea firma, adica un fel de tree. Xmlul are 3 niveluri: Managers urmat de Sales, Development si Administrative, fiecare la randul lui avand alte diverse categorii de angajati. Fiecare categorie nu poate avea alta subcategorie.


<Organigram>
<Managers ID="managersDepartment">
<Sales ID="salesDepartment">
<Salesmen ID="salesMen">
</Salesmen>
</Sales>
<Development ID="developmentDepartment">
<TeamLeaders ID="teamLeaders">
</TeamLeaders>
<Developers ID="developers">
</Developers>
</Development>
<Administrative ID="administrativeDepartment">
<HumanResources ID="humanResources">
</HumanResources>
<AssistantManagers ID="assistantManagers">
</AssistantManagers>
</Administrative>
</Managers>
</Organigram>

De fapt din acest Xml sa obtin urmatorul Html.


<h1 style="padding-left: 10px;">
Managers
</h1>
<h2 style="padding-left: 40px;">
Sales
</h2>
<h3 style="padding-left: 80px;">
<hr />
Salesmen
<hr />
</h3>
<h2 style="padding-left: 40px;">
Development
</h2>
<h3 style="padding-left: 80px;">
<hr />
TeamLeaders
<hr />
</h3>
<h3 style="padding-left: 80px;">
<hr />
Developers
<hr />
</h3>
<h2 style="padding-left: 40px;">
Administrative
</h2>
<h3 style="padding-left: 80px;">
<hr />
HumanResources
<hr />
</h3>
<h3 style="padding-left: 80px;">
<hr />
AssistantManagers
<hr />
</h3>

Pentru fiecare nivel din XML va exista un template: Level1, Level1 si Level2. Developerul de la design time va putea avea posibilitatea sa defineasca fiecare template. Pentru a genera html-ul de mai sus, controlul a fost scris astfel:



<my:MyCompositeDataBoundControl runat="server" ID="myCompositeDataBoundControl" DataSourceID="ObjectDataSource1">
<Level0>
<h1 style="padding-left: 10px;">
<%# Eval("Name") %>
</h1>
</Level0>
<Level1>
<h2 style="padding-left: 40px;">
<%# Eval("Name") %>
</h2>
</Level1>
<Level2>
<h3 style="padding-left: 80px;">
<hr />
<%# Eval("Name") %>
<hr />
</h3>
</Level2>
</my:MyCompositeDataBoundControl>

In metoda abstracta CreateChildControls se instantiaza fiecare template. Daca momentul este la dataBinding == true, atunci templaturile se aleg in functie de starea fiecarui dataItem din dataSource. Odata ales, trebuie intr-un fel a se memora faptul ca un anumit template reprezinta un anumit item si acest lucru se face in ViewState. Apoi, cand dataBinding e false ( nu s-au reluat datele dintr-un mediu de stocare, controlul e reconstruit din ViewState), atunci in functie de ce s-a memorat la dataBinding = true, se reface controlul. Metoda returneaza numarul de dataItem-uri din dataSource.



namespace CDataBoundControls
{
public class MyCompositeDataBoundControl : CompositeDataBoundControl
{
private ITemplate _level0;
[PersistenceMode(PersistenceMode.InnerProperty),
TemplateContainer(typeof(Item))]
public ITemplate Level0
{
get { return _level0; }
set { _level0 = value; }
}

private ITemplate _level1;
[PersistenceMode(PersistenceMode.InnerProperty),
TemplateContainer(typeof(Item))]
public ITemplate Level1
{
get { return _level1; }
set { _level1 = value; }
}


private ITemplate _level2;
[PersistenceMode(PersistenceMode.InnerProperty),
TemplateContainer(typeof(Item))]
public ITemplate Level2
{
get { return _level2; }
set { _level2 = value; }
}
protected int ItemCount
{
get { return (int)ViewState["@itemCount"]; }
set { ViewState["@itemCount"] = value; }
}
protected string[] Templates
{
get { return (string[])ViewState["Templates"]; }
set { ViewState["Templates"] = value; }
}
protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
if (dataSource == null)
{
ItemCount = 0;
return 0;
}

if (dataBinding)
{
int count = 0;
int itemIndex = 0;
var templates = new List<string>();
foreach (var dataItem in dataSource)
{
if (dataItem is Department)
{
var department = (Department)dataItem;

ITemplate template = null;
if (Level0 != null && department.Level == 0)
{
template = Level0;
templates.Add("Level0");
}
else
if (Level1 != null && department.Level == 1)
{
template = Level1;
templates.Add("Level1");
}
else
if (Level2 != null && department.Level == 2)
{
template = Level2;
templates.Add("Level2");
}
else template = null;


var item = new Item(department, itemIndex++);
if (template != null)
{
count++;
template.InstantiateIn(item);
Controls.Add(item);
if (dataBinding)
item.DataBind();

}

}
}
ItemCount = count;
Templates = templates.ToArray();
DataBind(false);
return count;
}
else
// the control is build from viewstate
{
ITemplate template = null;
for (int i = 0; i < ItemCount; i++)
{
string templateName = Templates[i];
switch (templateName)
{
case "Level0":
{
template = Level0;
break;
}
case "Level1":
{
template = Level1;
break;
}
case "Level2":
{
template = Level2;
break;
}
default:
{
break;
}
}
if (template != null)
{
Item item = new Item(null, i);
template.InstantiateIn(item);
Controls.Add(item);
}
}

return ItemCount;
}
}
}
}

Download Source Code and Play

03 iunie 2008

Operatorul APPLY in SQL 2005

CREATE TABLE dbo.Items
(
ID int IDENTITY(1,1) NOT NULL,
Name varchar(50) NOT NULL,
CONSTRAINT PK_Items PRIMARY KEY (ID ASC)
)
-- Ceva valori
GO
INSERT INTO Items VALUES ('X')
INSERT INTO Items VALUES ('Y')
INSERT INTO Items VALUES ('Z')

-- O functie care returneaza itemurile care au id-ul mai mic decat @ItemID (cele anterioare unui anumit item)
GO
CREATE FUNCTION [dbo].[getPreviousItems]
(
@ItemID int
)
RETURNS @PreviousItemTab TABLE
(
ID int,
Name varchar(50)
)
AS

BEGIN

INSERT INTO @PreviousItemTab
SELECT *
FROM dbo.Items AS i
WHERE i.ID < @ItemID

RETURN
END


Operatorul APPLY implica doi operanzi, primul operand (LEFT) actioneaza ca si input pentru cel de-al doilea (RIGHT). Pentru fiecare linie din rezultatul operandului LEFT se va evalua operandul RIGHT

Operatorul APPLY e de doua tipuri: CROSS APPLY si OUTER APPLY. Rezultatul lui CROSS APPLY nu va include liniile pentru care LEFT returneaza NULL in urma evaluarii, iar OUTER APPLY va include si aceste linii, pentru care evaluarea e nula



SELECT i.Name as Input,
f.*
FROM Items AS i
CROSS APPLY dbo.getPreviousItems(i.ID) AS f
ORDER BY i.ID

Pe acest exemplu, operandul RIGHT este rezultatul evaluarii SELECT i.Name FROM Items as I ORDER BY i.ID, iar operandul LEFT este dbo.getPreviousItems(i.ID) AS f, adica acea functie care returneaza item-urile anterioare unuia.

Rezultatul operatorului APPLY va fi format din coloanele Name din RIGHT precum si coloanele ID si Name din LEFT. Nu vor fi nume anonime sau generate, ci cele definite fie de tabela fie de functie.

Daca functia getPreviousItems returneaza doua sau mai multe randuri pentru inputul RIGHT, atunci Name va fi repetat pe fiecare linie. Daca e folosit CROSS APPLY, inputul nu va fi inclus in cazul in care functia returneaza NULL (Itemul nu are itemi anteriori). Daca e folosit OUTER, atunci rezultatul include si id-urile care nu au item-uri anteriori


Deci pentru CROSS APPLY rezulatul ar fi:

Input ID Name
--------- ---------------------
Y 1 X
Z 1 X
Z 2 Y

Iar pentru OUTER APPLY e:

Input ID Name
--------- ---------------------
X NULL NULL
Y 1 X
Z 1 X
Z 2 Y

19 aprilie 2008

IsPostBack

MSDN spune ca proprietatea IsPostBack e o valoare care indica daca pagina este creata pentru a fi raspunsul postbackului unui client sau pagina este creata si accesata pentru prima data.

Cuvantul cheie e postback, de fapt ce inseamna acest postback. Perceptia generala e ca IsPostBack e true atunci cand utilizatorul da click pe un buton sau pe un control dintr-o anumita pagina, care are setata proprietatea AutoPostBack = true (dropdown de exemplu).. sau hai sa zicem pe orice control cu evenimente care se instantiaza pe server in urma unei actiuni a utilizatorului in fereastra browserului.

Mai altfel zis, Popescu, avid de monden, deschide browseru, scrie http://www.web.com/Clasamente.aspx, ii apare pagina frumoasa, care il invita sa dea click pentru a vedea clasamentul celor mai naspa vedete din Romania. Developerul presupune la PageLoad ca Page.IsPostBack e true, adica Popescu sigur avea deja pagina incarcata in browser, inainte de a accesa clasamentul celor mai naspa vedete din Romania.

Scenariul e de fapt ca browserul lui Popescu face o cerere GET (IsPostBack = false), serverul ii trimite browserului lui Popescu un HTML generat de ASP .Net, gecko sau trident sau care o fi raspunzator, se apuca de desenat chestii din HTML, apoi browserul lui Popescu e instruit sa faca o cerere POST(IsPostBack = true), pentru ca Popescu a dat click pe butonul care ii spunea lui ca daca da click acolo va vedea clasamentul celor mai naspa vedete din Romania. Clasamentul se formeaza in urma unor voturi, dar nu sta nimeni sa le numere, deci sigur tre sa se treaca prin ASP .Net, sa se ocupe el de calcule.

Developerul presupune ca Popescu nu poate ajunge la clasament decat daca a intrat pe pagina http://www.web.com/Clasamente.aspx, mai ales ca el, developerul, nu ii ofera nici un link de genul http://www.web.com/Clasamente/CeleMaiNaspaVedeteDinRO.aspx. Developerul e foarte convins ca la momentul cererii clasamentului, Popescu sigur a facut anterior o cerere la http://www.web.com/Clasamente.aspx.

Nu prea e asa. Popescu poate sa ajunga la clasament, chiar daca nu are acces la URLul clasamentului, iar developerul a presupus gresit. Popescu poate folosi un tool de gen Fiddler , pentru a crea o cerere POST sau, daca stie putin HTML, isi creeaza un fisier care sa contina un form cu anumite inputuri. Browserul lui va crea o cerere POST similara cu cea reala din punct de vedere al continutului specific cererii POST, unde se afla parametrii si valorile lor. Singurul lucru la care trebuie sa fie atent Popescu e sa includa si parametrul __VIEWSTATE, corespunzator paginii. Mai mult, Smith din Texas, stiind valoarea lui __VIEWSTATE, poate construi aceeasi cerere ca a lui Popescu.

Diverse valori ale lui IsPostBack, pentru situatii confuze:

1. se face un GET la http://www.web.com/Clasamente.aspx, pagina va contine un <a href="/Clasamente.aspx">Clasamente</a> si userul urmareste linkul.

    Se cam incadreaza in ceea ce a zis MSDN, adica am dat un click intr-o pagina si am primit aceeasi pagina si IsPostBack ar trebuie sa fie true. De fapt, urmarind linkul, s-a creat o cerere GET, deci IsPostBack va fi false.
2. Se creeaza un fisier HTML care sa contina ceva de genul
<form method="post" action="http://www.web.com/Clasamente.aspx" id="form1">
<input type="hidden"
name="__VIEWSTATE"
id="__VIEWSTATE"
value="/wEPDwUJNzgzNDMwNTMzZGSzEhAHmN2wh/fD7Vy8+yI92wZpSw=="/>
</form>
<script>
document.getElementById("form1").submit();
</script>

Intr-adevar aici pagina nu a fost creata pentru prima data. Pentru a obtine valoarea lui __VIEWSTATE, ar fi trebuit sa creez mai intai o cerere GET. IsPostBack va fi true, pentru ca browserul va creea o cerere POST si va include si parametrul __VIEWSTATE.

Dar daca cineva imi va trimite acest fisier HTML, ce poate fi comparat cu un shortcut in Windows, senzatia ca eu as fi facut PostBack e cam falsa. Tehnic e postback.

Sa urmam sfatul ca ViewState nu trebuie sa contina nici un fel de informatie legata de securitate, iar ceea ce se face in urmatorul test, la fel, nimic legat de securitate


if (!Page.IsPostBack)
{

}
else
{

}

14 aprilie 2008

Un bug in LINQ

Am dat de un mic bug in LINQ to SQL, ce m-a facut sa-mi regandesc o abordare dintr-un anumit proiect. Eroarea apare in interiorul DataContext si anume la nivelul asocierii dintre clasele mapate pe tabele in scopul de a evidentia relatia 1:n.

Sa explic insa cu un exemplu.

Se da o baza de date cu trei tabele, o tabela cu categorii, o tabela cu produse si o tabela History, care contine istoria operatiilor pe celelalte doua. Pentru o anumita inregistrare din Products sau Categories vor exista una sau mai multe intrari(sau niciuna) in tabela History. Stiind un anumit HistoryGUID din Products sau Categories se pot afla inregistrarile din History, adica istoria operatiilor facute pentru o anumita inregistrare din oricare tabele. Cum HistoryGUID este generat automat de SQL Server cu newid(), la inserarea unei noi inregistrari intr-o tabela din cele doua, atunci e asigurata unicitatea lui la nivel de baza de date, conditie necesara pentru a identifica corect cumulul de inregistrari in History asociate lui. Natural este sa se construiasca anumite constrangeri la nivel de logica a structurii. Fiecare HistoryGUID din Categories sau Products este unic, deci se pot adauga chei unice la nivel de tabele formate din acest HistoryGUID. Am preferat sa las cheia primara un int identity(ID), pentru ca aceasta parte cu istoria este doar o particularitate a bazei de date. Pornind de la aceste chei unice se pot adauga ,la fel de natural, chei straine intre tabela Categories si tabela History si intre tabela Products si tabela History.



Translatand in model OOP, se obtin afirmatiile:



- o categorie contine o lista de istorii
- un produs contine o lista de istorii
- o istorie are un produs
- o istorie are o categorie.
Aceste relatii sunt generate si de LINQ to DBML

Ultimile doua afirmatii sunt contradictorii, adica o inregistrare din History nu poate avea corespondent atat in Categories cat si in Products. Ori se inregistreaza o actiune pentru o anumita categorie, ori se inregistreaza o anumita actiune pentru un anumit produs. Lucru sanctionat si de SQL server, prin faptul ca nu esti lasat sa adaugi o inregistrare in History, decat daca exista acel uniqueidentifier atat in Categories cat si in Products la nivelul coloanei HistoryGUID. Daca ar fi acelasi GUID atat in Categories cat si in Products, atunci nu s-ar mai putea identifica cumulul inregistrarilor in History pentru acel GUID, ceea ce contrazice ideea de a pastra istoria operatiilor pentru o anumita inregistrare dintr-o anumita tabela. Asadar aceste chei straine nu ajuta si nu vor mai exista. Insa la nivel de ORM, stergerea lor inseamna si eliminarea primelor doua afirmatii (cel putin la generarea automata de Visual Studio 2008 DataContext-ului). Pentru a pastra insa aceasta modelare, le adaug manual. Observ ca si de data aceasta nu am scapat de problema faptului ca un obiect de tip History are atat un produs cat si o categorie. Sa zicem DBMLu se afla intr-o librarie separata de cea in care va fi folosit si modific accesorul proprietilor Product si Category din public in internal.
Acum, toate bune si frumoase pana a venit timpul si sa folosesc acest model. Pentru un anumit Product vreau sa adaug un History.



private static void InsertProductHistory(int productID)
{
LinqBugDataContext dc = new LinqBugDataContext();
Product product = dc.Products.FirstOrDefault(p => p.ID == productID);
History newHistory = new History();
product.Histories.Add(newHistory);
dc.SubmitChanges();
}

Si obtin urmatorul mesaj de eroare



Unhandled Exception: System.InvalidCastException: Specified cast is not valid.
at System.Data.Linq.IdentityManager.StandardIdentityManager.SingleKeyManager`
2.TryCreateKeyFromValues(Object[] values, V& v)
at System.Data.Linq.IdentityManager.StandardIdentityManager.IdentityCache`2.F
ind(Object[] keyValues)
at System.Data.Linq.IdentityManager.StandardIdentityManager.Find(MetaType typ
e, Object[] keyValues)
at System.Data.Linq.CommonDataServices.GetCachedObject(MetaType type, Object[
] keyValues)
at System.Data.Linq.ChangeProcessor.GetOtherItem(MetaAssociation assoc, Objec
t instance)
at System.Data.Linq.ChangeProcessor.BuildEdgeMaps()
at System.Data.Linq.ChangeProcessor.SubmitChanges(ConflictMode failureMode)
at System.Data.Linq.DataContext.SubmitChanges(ConflictMode failureMode)
at System.Data.Linq.DataContext.SubmitChanges()
at LinqBug.Program.InsertProductHistory(Int32 productID) in Z:\blogger\LinqBu
g\LinqBug\Program.cs:line 32
at LinqBug.Program.Main(String[] args)

Apare la:

internal override bool TryCreateKeyFromValues(object[] values, out V v)
{
object obj2 = values[this.offset];
if ((obj2 == null) && !this.isKeyNullAssignable)
{
v = default(V);
return false;
}
v = (V) obj2;
return true;
}

Asa ca presupun ca atunci cand LINQ isi formeaza cele necesare cand isi creaza tipurile, relatiile si toate cele, a cam dat de pamant in acest scenariu. Nu stiu cum as putea rezolva asta, dar important e ca stiu ca nu se poate si pe viitor evit ideea pana MS revine cu o rezolvare.

30 martie 2008

Reindexare in SQL 2000

Multe aplicatii ASP .Net au in spate si un server de baze de date. Ce tre’ sa faci atunci cand pagini cu diverse rapoarte se incarca tot mai greu de la o saptamana la alta, primesti erori de timeout tot mai des pe mail, iar clientii incep sa devina din ce in ce frustrati? Incepi sa gugalesti:).

Presupun ca pe tabelele alea mari sunt deja construiti niste indecsi, iar indecsii au peste 1000 pagini (adica peste 8000 KB).

O idee ar fi sa se reverifice cum sunt construiti indecsii. Daca raportul e construit pe baza unei interogari care ordoneaza randurile dupa col1 ASC si col2 DESC si raportul e cerut destul de des, atunci indexul ar trebuie sa fie construit la fel (adica dupa col1 ASC si col2 DESC). Daca indexul are un fill factor de 100% (sau 0%) si pe tabela se fac la fel de des si operatii de insert,update sau delete atunci ar trebui reconsiderata valoarea acestui fill factor la una sub 100%. Cum un raport returneaza si alte coloane in afara de cele folosite in clause WHERE sau la ORDER BY ori GROUP BY, atunci ar fi bine ca indexul sa fie de tip CLUSTER (paginile frunza din B-TREE contin si celelalte coloane, nu numai datele din coloanele indexului).

Dupa verificarea asta, periodic trebuie facuta o reindexare, pentru ca ordinea fizica a paginilor se modifica o data cu stergerea, inserarea sau modificarea datelor din tabela si nu mai coincide cu ordinea paginilor din index.

Reindexarea se poate face prin trei feluri:

  • stergere si recreare index (cam cu batai de cap atunci cand alte tabele referentiaza la coloanele din index)
  • DBCC DBREINDEX – operatie offline, adica accesul la tabela e exclusiv
  • DBCC INDEXDEFRAG – operatie online, utilizatorii sitului nu trebuie sa astepte pana se termina reindexarea

Voi exemplifica o metodologie de a verifica si reindexa anumiti indecsi pe o tabela dintr-un SQL SERVER 2000.

1. sp_helpindex

exec sp_helpindex ‘Products’

Afiseaza numele indexului, descrierea si cheile indecsilor din tabela Products. Se pot vedea rapid daca indecsii sunt construiti bine pentru interogarile folosite.

2. DBCC SHOWCONTIG

DBCC SHOWCONTIG('Products') WITH ALL_INDEXES



- Pages Scanned................................................: 7025
- Extents Scanned..............................................: 887
- Extent Switches..............................................: 4046
- Avg. Pages per Extent........................................: 7.9
- Scan Density [Best Count:Actual Count].......................: 21.72% [879:4047]
- Logical Scan Fragmentation ..................................: 33.84%
- Extent Scan Fragmentation ...................................: 51.86%
- Avg. Bytes Free per Page.....................................: 2247.2
- Avg. Page Density (full).....................................: 72.24%

Sunt peste 7000 de pagini pentru primul index si dupa cum zice Microsoft, se merita facuta reindexarea, daca e nevoie, pentru ca numarul e mai mare de 1000. Ca se decizi daca e nevoie, urmatorii parametri trebuie sa aiba valorile:

  • Scan Density: cat mai aproape de 100%
  • Logical Scan Fragmentation si Extent Scan Fragmentation cat mai aproape de 0%

3. fill factor

E bine de stiut care e fill factor-ul inainte de a face reindexarea. Probabil ca cel care a creat indexul sau cel care a facut o reindexare in trecut, a ales o valoarea bine gandita.


select OrigFillFactor
from sysindexes
where name='IX_ProductID'

4. DBCC DBREINDEX

DBCC DBREINDEX (‘Products’, IX_ProductID,90);

Aleg DBREINDEX, o operatie offline, cum am zis, pentru ca reconstruieste si statisticile, iar daca serverul are mai multe procesoare, pentru indecsii foarte mari si foarte fragmentati, atunci operatia se executa mult mai repede.

5. se repeta SHOWCONTIG si DBREINDEX

Pentru ca e posibil ca DBREINDEX sa nu faca ceea ce ar trebui sa faca, adica sa-mi aduca parametrii la valorile care le doresc eu, trebuie repetate DBREINDEX si reverificat rezultatul cu SHOWCONTIG. In principiu, se mai scad sau se mai aduna la fill factor 2 puncte, ori cu putin noroc, reexecutata cu aceeasi valoare sa se obtina ceea ce s-a vrut de la inceput, un index cu paginile aranjate asa cum e ordinea lor fizica.

21 martie 2008

HTML Parser sau Expresii Regulate




Extrag informatie dintr-o pagina si ma intreb cum anume sa procedez. Sa folosesc expresii regulate sau un parser html. Norocul meu e ca am gasit unul deja implementat de un anume tip Alex Chudnovsky, iar .Net imi ofera suport si pentru expresii regulate.

Am luat ca test prima pagina a sitului ejobs, din care vreau sa extrag lista de orase din tagul HTML select.



<select name="oras" class="input12" style="width:130px;" >
<option value="">Toate orasele</option>
<option value="262" >Abrud</option>
...
<option value="298" >Zlatna</option>
</select>

Expresii Regulate
Am avut nevoie de doua expresii regulate. Una pentru a extrage toate tagurile <option></option> din acel <select>

(?<selectOrasInput> // constructie pentru a marca grupul in RegExp
<select //inceput de tag
((?:.|\n)*?) //orice intre tag si atributul name
name=\"oras\" //atributul name trebuie sa aiba valoarea oras
((?:.|\n)*?)
>
(?<options>(?:.|\n)*?) // marchez grupul cu de optiuni
</select>
)

...si una pentru a extrage valoarea si textul din optiune

value=\"(?<Value>(?:.|\n)*?)\" // value=" 262"
(?:.|\n)*?
>(?<Text>(?:.|\n)*?)</option> // > Abrud</option>





HTML Parser

while ((oChunk = HtmlParserMajestic.ParseNext()) != null) // parse all html
{
if (oChunk.GetParamValue("name") == "oras") // parser on select node
{
while ((oChunk = HtmlParserMajestic.ParseNext()) != null) // parse first option
{
if (oChunk.sTag == "select" && oChunk.oType == HTMLchunkType.CloseTag)
break; // exit from parsing options
if (oChunk.sTag == "option" && oChunk.oType == HTMLchunkType.OpenTag)
{
option = new SelectOption();
option.Value = oChunk.GetParamValue("value"); // get value
oChunk = HtmlParserMajestic.ParseNext(); // parse to text
option.Text = oChunk.oHTML.Trim(); // get text
oChunk = HtmlParserMajestic.ParseNext(); // parse to end tag
citiesFromParser.Add(option); // add new option
}
}
}
}

Din ce am testat, expresiile regulate sunt pana la 8, 9 ori mai lente decat parserul HTML( 125 milisecunde expresiile, 15 milisecunde parserul). Daca as vrea sa extrag numere de telefon, emailuri, date calendaristice din textul paginii HTML, parserul nu-mi poate fi de folos decat pana la un anumit punct sau deloc, dar folosind expresii regulate, le pot obtine cu siguranta.



Download Source Code And Play

12 ianuarie 2008

LINQ to XML in ASP .Net



Un alt mod de a stoca si manipula date este familia XML. Datele din fisiere sunt constranse sa respecte un anumit model, prin schemele XSD. Cu XPath se colecteaza date, iar cu XSLT fisierul sau setul de noduri se "converteste" in alt model de reprezentare (HTML, XML,..).

Am imprumutat o parte din baza de date Northwind din SQL si am construit un fisier XML care contine un model relational. Astfel exista o lista de noduri de tip "produs" (tabela Products),o lista de tip "categorie" (tabela Categories) si o lista de noduri de tip "furnizor" (tabela Suppliers). Fiecare lista contine cate o cheie primara (xs:key). Pentru lista "Products" sunt definite chei straine care referentiaza la elemente din listele "Categories" sau "Suppliers" (xs:keyref). Cu xs:unique s-a constrans ca numele categoriilor/furnizorilor sa fie unic (Unique Constraint in SQL).



Fisier xml: lista de produse, categorii si furnizori.


<?xml version="1.0" encoding="utf-8" ?>
<EstWind
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://localost/LinqToXml EstWind.xsd">
<Products>
<Product ProductID="1" SupplierID="1" CategoryID="1" ProductName="Chai"/>
<Product ProductID="2" SupplierID="1" CategoryID="1" ProductName="Chang"/>
<Product ProductID="3" SupplierID="1" CategoryID="2" ProductName="Aniseed Syrup"/>
<Product ProductID="4" SupplierID="2" CategoryID="2" ProductName="Chef Anton's Cajun Seasoning"/>
<Product ProductID="5" SupplierID="2" CategoryID="2" ProductName="Chef Anton's Gumbo Mix"/>
<Product ProductID="6" SupplierID="3" CategoryID="2" ProductName="Grandma's Boysenberry Spread"/>
<Product ProductID="7" SupplierID="3" CategoryID="7" ProductName="Uncle Bob's Organic Dried Pears"/>
<Product ProductID="8" SupplierID="3" CategoryID="2" ProductName="Northwoods Cranberry Sauce"/>
<Product ProductID="9" SupplierID="4" CategoryID="6" ProductName="Mishi Kobe Niku "/>
<Product ProductID="10" SupplierID="4" CategoryID="8" ProductName="Ikura"/>
</Products>
<Suppliers>
<Supplier SupplierID="1" CompanyName="Exotic Liquids"/>
<Supplier SupplierID="2" CompanyName="New Orleans Cajun Delights"/>
<Supplier SupplierID="3" CompanyName="Grandma Kelly's Homestead"/>
<Supplier SupplierID="4" CompanyName="Tokyo Traders"/>
</Suppliers>
<Categories>
<Category CategoryID="1" CategoryName="Beverages"/>
<Category CategoryID="2" CategoryName="Condiments"/>
<Category CategoryID="6" CategoryName="Meat/Poultry"/>
<Category CategoryID="7" CategoryName="Produce"/>
<Category CategoryID="8" CategoryName="Seafood"/>
</Categories>
</EstWind>



Schema de validare a XML-ului: modele de key primare, unice si straine.



<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified"
elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">

<xs:element name="EstWind" type="EstWindType">

<!-- + Chei straine -->
<!--
Atributul CategoryID al oricarui Product
accepta valori numai din setul format
din valorile atributului CategoryID din
oricare nod Category.
-->
<xs:keyref name="CaregoryRef" refer="CategoriesKey">
<xs:selector xpath=".//Product"/>
<xs:field xpath="@CategoryID"/>
</xs:keyref>

<xs:keyref name="SupplierRef" refer="SuppliersKey">
<xs:selector xpath=".//Product"/>
<xs:field xpath="@SupplierID"/>
</xs:keyref>

<!-- |_ Chei straine -->

<!-- + Chei primare -->
<!--
Valorile atributului ProductID sunt unice
Obs.: Nodul Products contine doar elemente de tip Product.

E valida si expresia xpath .//Product.

Se defineste un set de noduri, iar pe acel set
se alege un atribut sau un nod
care sa reprezinte cheia unica.
-->
<xs:key name="ProductsKey">
<xs:selector xpath="Products/*"/>
<xs:field xpath="@ProductID"/>
</xs:key>


<xs:key name="CategoriesKey">
<xs:selector xpath="Categories/*"/>
<xs:field xpath="@CategoryID"/>
</xs:key>


<xs:key name="SuppliersKey">
<xs:selector xpath="Suppliers/*"/>
<xs:field xpath="@SupplierID"/>
</xs:key>

<!-- |_ Chei primare -->


<!-- + Chei unice -->

<!-- Diferenta dintre xs:unique si xs:key
e ca xs:unique accepta valori null -->

<xs:unique name="ProductNameUnique">
<xs:selector xpath=".//Product"/>
<xs:field xpath="@ProductName"/>
</xs:unique>

<xs:unique name="CompanyNameUnique">
<xs:selector xpath=".//Supplier"/>
<xs:field xpath="@CompanyName"/>
</xs:unique>

<xs:unique name="CategoryNameUnique">
<xs:selector xpath=".//Category"/>
<xs:field xpath="@CategoryName"/>
</xs:unique>

<!-- |_ Chei unice -->

</xs:element>

<!-- + Root Type -->

<xs:complexType name="EstWindType">
<xs:sequence>
<xs:element minOccurs="1" maxOccurs="1" name="Products" type="ProductsType"/>
<xs:element minOccurs="1" maxOccurs="1" name="Suppliers" type="SuppliersType"/>
<xs:element minOccurs="1" maxOccurs="1" name="Categories" type="CategoriesType"/>
</xs:sequence>
</xs:complexType>

<!-- |_ Root Type -->


<!-- + List Types -->

<xs:complexType name="ProductsType">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" name="Product" type="ProductType"/>
</xs:sequence>
</xs:complexType>


<xs:complexType name="SuppliersType">
<xs:sequence>
<xs:element minOccurs="1" maxOccurs="unbounded" name="Supplier" type="SupplierType"/>
</xs:sequence>
</xs:complexType>


<xs:complexType name="CategoriesType">
<xs:sequence>
<xs:element minOccurs="1" maxOccurs="unbounded" name="Category" type="CategoryType"/>
</xs:sequence>
</xs:complexType>

<!-- |_ List Types -->


<!-- + Basic Types -->

<xs:complexType name="ProductType">
<xs:attribute name="ProductID" type="xs:unsignedByte" use="required" />
<xs:attribute name="SupplierID" type="xs:unsignedByte" use="required" />
<xs:attribute name="CategoryID" type="xs:unsignedByte" use="required" />
<xs:attribute name="ProductName" type="xs:string" use="required" />
</xs:complexType>


<xs:complexType name="SupplierType">
<xs:attribute name="SupplierID" type="xs:unsignedByte" use="required" />
<xs:attribute name="CompanyName" type="xs:string" use="required" />
</xs:complexType>


<xs:complexType name="CategoryType">
<xs:attribute name="CategoryID" type="xs:unsignedByte" use="required" />
<xs:attribute name="CategoryName" type="xs:string" use="required" />
</xs:complexType>

<!-- |_ Basic Types -->


</xs:schema>


Controlul TreeView interactioneaza cu modelul de date din XML cu ajutorul unui control XmlDataSource. Prin intermediul proprietatii Transform (XmlDataSource), al carei continut e un XSLT, se pot crea diferite forme ale modelului. Fiecare TreeView are definite databinding-uri, dupa model sau formele modelului, intre proprietatile nodurilor treeview-ului si proprietatile elementelor modelului. In printscreen-ul de mai jos primul treeview reprezinta modelul, celelalte doua reprezinta o data produsele grupate dupa categoria din care fac parte si o data produsele grupate dupa furnizorul lor.


Acelasi fisier XML, reprezentat in forme diferite

XmlDataSource cu foaie XSLT, pentru a obtine produsele grupate pe categorie


<asp:XmlDataSource ID="XmlDataSource2" runat="server" DataFile="~/EstWind.xml">
<Transform>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<ProductsByCategory>
<xsl:for-each select="EstWind/Categories/Category">
<xsl:sort select="@CategoryName" order="ascending" />
<xsl:element name="Category">
<xsl:attribute name="DisplayName" >
<xsl:value-of select="@CategoryName" />
</xsl:attribute>

<xsl:attribute name="ID">
<xsl:value-of select="concat('category;',@CategoryID)"/>
</xsl:attribute>

<xsl:for-each select="//Product[@CategoryID=current()/@CategoryID]">
<xsl:sort select="@ProductName" order="ascending" />

<xsl:element name="Product">
<xsl:attribute name="DisplayName">
<xsl:value-of select="@ProductName" />
</xsl:attribute>

<xsl:attribute name="ID">
<xsl:value-of select="concat('product;',@ProductID)"/>
</xsl:attribute>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:for-each>
</ProductsByCategory>
</xsl:template>
</xsl:stylesheet>
</Transform>
</asp:XmlDataSource>

Intre componentele unei pagini ASP .Net trebuie sa se comunice. De exemplu in urma selectarii unui produs din treeview, o alta componenta afiseaza detalii despre acel produs. Sau in urma selectarii unei categorii afiseaza o lista detaliata a produselor acelei categorii. Evenimentele din treeview nu ofera prea multe despre model. Un treeview nu stie ca ai selectat un produs sau o categorie, ci stie ca ai selectat un anumit nod de pe un anumit nivel care un id, text, valoare. Asadar as vrea sa stiu ca am selectat produsul X sau categoria Y din fisierul XML.



Clasele C# care reprezinta modelul din XML.


// Acest tip de clase este introdus in C# 3.0 pentru a facilita interogarile LINQ
public class Product
{
public sbyte ProductID { get; set; }
public string ProductName { get; set; }
public Supplier Supplier { get; set; }
public Category Category { get; set; }
}
public class Category
{
public sbyte CategoryID { get; set; }
public string CategoryName { get; set; }
public List<Product> Products{ get; set; }
}
public class Supplier
{
public sbyte SupplierID { get; set; }
public string CompanyName { get; set; }
public List<Product> Products { get; set; }
}

Pentru a defini evenimentele in controale ASP .Net e de indicat a se folosi colectia Events. E de preferat atunci cand un control are foarte multe evenimente. Colectia Events este o lista de delegati de tip (cheie, (delegat1, ... delegatn)) si se foloseste in conjunctie cu instructiunile add/remove.



Definirea evenimentului

   
static readonly object ProductSelectedEventKey = new object();
public event EventHandler<ProductSelectedEventArgs> ProductSelected
{
add { Events.AddHandler(ProductSelectedEventKey, value); }
remove { Events.RemoveHandler(ProductSelectedEventKey, value); }
}
protected virtual void OnProductSelected(ProductSelectedEventArgs e)
{
EventHandler<ProductSelectedEventArgs>
handler = Events[ProductSelectedEventKey] as EventHandler<ProductSelectedEventArgs>;
if (handler != null)
handler(this, e);
}
// argumentul evenimentul va contine produsul selectat
public class ProductSelectedEventArgs : EventArgs
{
private Product product;
public ProductSelectedEventArgs(Product product)
{
this.product = product;
}
public Product Product
{
get { return product; }
}
}




Ultimul pas e definirea momentului cand e declansat evenimentul precum si interogarea fisierului XML pentru a obtine produsul sau categoria. Aceasta se va face cu "LINQ to XML". Cum am definit in data bindings, ID-urile nodurilor au formele "product;@ProductID" sau "category;CategoryID". Cunoscand ProductID sau CategoryID, se interogheaza cu LINQ un obiect XDocument. Partea buna e ca nu e necesar sa ai cunostine despre XPath, navigatori, iteratori ca sa interoghezi fisiere XML. Practic cu LINQ codul exprima ce am nevoie sa iau si nu cum sa iau.



Interogare XML cu LINQ: obtinerea unui Product

 
private Product LoadProduct(string productID)
{
var products = from product in EstWindDocument.Descendants("Product")
where product.Attribute("ProductID").Value == productID

select new Product
{
ProductName = product.Attribute("ProductName").Value,
ProductID = SByte.Parse(product.Attribute("ProductID").Value),
Category = (from category in EstWindDocument.Descendants("Category")
where category.Attribute("CategoryID").Value == product.Attribute("CategoryID").Value
select new Category
{
CategoryID = SByte.Parse(category.Attribute("CategoryID").Value),
CategoryName = category.Attribute("CategoryName").Value
}).FirstOrDefault<Category>(),
Supplier = (from supplier in EstWindDocument.Descendants("Supplier")
where supplier.Attribute("SupplierID").Value == product.Attribute("SupplierID").Value
select new Supplier
{
SupplierID = SByte.Parse(supplier.Attribute("SupplierID").Value),
CompanyName = supplier.Attribute("CompanyName").Value
}).FirstOrDefault<Supplier>()
};
return products.FirstOrDefault<Product>();
}


private Category LoadCategory(string categoryID)
{
var categories = from category in EstWindDocument.Descendants("Category")
where category.Attribute("CategoryID").Value == categoryID
select new Category
{
CategoryID = SByte.Parse(category.Attribute("CategoryID").Value),
CategoryName = category.Attribute("CategoryName").Value,
Products = (
from product in EstWindDocument.Descendants("Product")
where product.Attribute("CategoryID").Value == category.Attribute("CategoryID").Value
select new Product
{
ProductName = product.Attribute("ProductName").Value,
ProductID = SByte.Parse(product.Attribute("ProductID").Value),
Category = (from pcategory in EstWindDocument.Descendants("Category")
where pcategory.Attribute("CategoryID").Value == product.Attribute("CategoryID").Value
select new Category
{
CategoryID = SByte.Parse(pcategory.Attribute("CategoryID").Value),
CategoryName = pcategory.Attribute("CategoryName").Value
}).FirstOrDefault<Category>(),
Supplier = (from supplier in EstWindDocument.Descendants("Supplier")
where supplier.Attribute("SupplierID").Value == product.Attribute("SupplierID").Value
select new Supplier
{
SupplierID = SByte.Parse(supplier.Attribute("SupplierID").Value),
CompanyName = supplier.Attribute("CompanyName").Value
}).FirstOrDefault<Supplier>()
}
).ToList<Product>()
};
return categories.FirstOrDefault<Category>();
}








Download Source Code And Play