Showing posts with label ASP.NET. Show all posts
Showing posts with label ASP.NET. Show all posts

Monday, June 6, 2011

Passed 70-515

As of today, I am certified for ASP.NET 4.0.

Learning from my previous experience, yesterday I drove to the test center and checked it out first. I don't understand why the same test center keeps moving from place to place! Every time I take an exam with them, they move. This time I ensure I know how to get there so everything was smooth. As usual, I was there an hour ahead and waiting outside for the time came.

There are 51 questions in total for 2.5 hours. I finished them about 1.75 hours. I am happy with my score. I could miss 1 or 2 questions. That's. My score is 970 out of 1000. The passing mark for this test is still at 700.

For some reason, I feel 4.0 is a lot easier than 3.5. But I am surprised to be tested by some jQuery questions. There were at least 4 questions in my test. Frankly, I don't find them related to ASP.NET. To me, it doesn't make any sense to have them in the test. jQuery is not mentioned in MS testing objective. Why am I being tested with jQuery syntax? Luckily I use jQuery and am able to answer them. Otherwise, it could be doomed. Besides MVC 2 and dynamic data stuff, I don't see any new stuff in the test. If you know 3.5 (including SP1) well and put some effort to learn more about MVC 2 and dynamic data, for sure you will be ready for the test.

Good luck to everyone who is preparing or going to take this test!

Thursday, May 19, 2011

WCF and Interface

Sometime ago, I had a few WCF services without using interface. As time goes by, some functionality need to expand. To avoid exposing all the service functionality to a single endpoint, implementing multiple interfaces is necessary so that multiple endpoints can be configured based on interface. Unfortunately, with this change, it broke every client call (JavaScript) in the old code.

In the code, the JavaScript function calls were all used and based on the library stub automatically generated by ASP.NET where the page uses ScriptManager to manage MS AJAX library. Thus the change at the contract name specified in the Web.config will automatically refresh the changes into the library.

Previously, there was no interface involved. The contract attribute of the endpoint defined in Web.config is directly pointing to the name of service itself. For example,

 <system.serviceModel> 
  
  <behaviors>
    <endpointBehaviors>
      <behavior name="Shipment.Order.WebAspNetAjaxBehavior">
      <enableWebScript />
  </behavior>
    ....
  <service name="Shipment.Order">  
     <endpoint address="" 
           behaviorConfiguration="Shipment.Order.WebAspNetAjaxBehavior"
           binding="webHttpBinding" 
           contract="Shipment.Order" />
  </service>  
  ...  
</system.serviceModel>  

Now, with the interface; the declaration of the endpoint contract is the interface instead of the service itself.

  <service name="Shipment.Order">  
     <endpoint address="" 
           behaviorConfiguration="Shipment.Order.WebAspNetAjaxBehavior"
           binding="webHttpBinding" 
           contract="Shipment.IOrder" />
  </service>  

In the page, the original JavaScript would call the service operation like this:

    Shipment.Order.Confirm(myId);

In order to work with the interface approach, the JavaScript has to use the interface to make a call:

    Shipment.IOrder.Confirm(myId);

Since the service has been placed into service sometime ago, the change to use interface is not a good idea. It affects a lot of pages that use this service. Instead of making the existing service to implement an interface, I derived a subclass from it so that the subclass inherits whatever its parent has. Then I simply configure an endpoint for this subclass instead of configuring multiple endpoints for the existing service. As a result, it works like a charm!

If you are having a problem with the interface or keep getting JavaScript error such as “xxx is NULL or not an Object” or “Cannot call method 'xxx' of undefined”. You would better look at the method you use in the JavaScript. It must be changed to use the interface because the JavaScript stub generated by ASP.NET is based on the contract defined in the Web.config. You could change the namespace by declaring [ServiceContract(Namespace = "xxx")] at your interface/class but your contract name won't be changed. You can examine the JavaScript stub by appending "/js" to the end of the service URL to compare the differences.

Thursday, April 14, 2011

URL Routing Causing to Load Login.aspx and SyntaxError

It is very strange. After the URL Routing module had been added into the non-MVC Web application, the application kept trying to load Login.aspx into the pages that don't need authentication and cause other syntax errors.

Uncaught SyntaxError: Unexpected token <
Register.aspx:63Uncaught Error: ASP.NET Ajax client-side framework failed to load.
Login.aspx:3Uncaught SyntaxError: Unexpected token <
jsdebug:1Uncaught ReferenceError: Type is not defined
Uncaught SyntaxError: Unexpected token <
Login.aspx:187Uncaught ReferenceError: WebForm_AutoFocus is not defined

After a few hours searching in code, I realized that the problem was caused by WebResource.axd. All the scrips for WebForm validation, focus, post back and etc were replaced by Login.aspx. WebResource.axd were instead trying to load Login.aspx.

Another interesting is that I duplicated some of user controls and pages to another temporary project for investigation but I am unable to re-produce the same behavior. With the same routing table, the same Web.config and a scale-down Global.ascx, Login.aspx won't be loaded when it is not being asked for. All JavaScripts are loaded as expected by WebResource.axd without problems. I still wonder what settings or events in the original application cause this problem.

Since this problem is caused by routing, one way to fix is to bypass the route. Adding RouteTable.Routes.RouteExistingFiles = true; won't fix the problem. Instead, a specific rule for ignoring a route is needed.


// For .NET 4.0
//RouteTable.Routes.Ignore("{resource}.axd/{*pathInfo}");  

// For .NET 3.5 SP1
RouteTable.Routes.Add(new Route("{resource}.axd/{*pathInfo}", new StopRoutingHandler()));

Tuesday, April 12, 2011

DateTime.Parse problem

Some of DateTime are taken from the user input and they are instantiated by the simaple DateTime constructor new DataTime(year, month, day);. When they are serialized into XML, the string will become like this:

   2001-07-08T00:00:00   

I tried to parse it back to DateTime from a XML value string. Unfortunately, I tried every method I know but kept getting String was not recognized as a valid DateTime error. For example,

  
    // Every method here causes String was not recognized as a valid DateTime error.  
  
    DateTime d = DateTime.Parse(query.Element("joinDate").ToString());
    
    string dateStr = query.Element("joinDate").ToString().Replace("T", "");
    DateTime d = XmlConvert.ToDateTime(dateStr, "yyyy-mm-dd hh:mm:ss");
    
    string dateStr = query.Element("joinDate").ToString().Replace("T", "");
    DateTime d = DateTime.ParseExact(dateStr, "yyyy-mm-dd hh:mm:ss", null);  
    
    string dateStr = query.Element("joinDate").ToString().Replace("T00:00:00", "");
    DateTime d = DateTime.ParseExact(dateStr, "yyyy-mm-dd", null);   
    

None of the above work for me. I also tried to follow the example/best practice discussed in MSDN but the error won't go away. Timezone is not my concern. As a matter of fact, my serialized XML value is not really represented in UTC time ( 2001-07-08T00:00:00 ). I guess it may be why MSDN suggestion won't work!

I finally gave up trying and use Regular Expression to parse the date fields and then convert it to DateTime manually. The following is my solution. I am sure that there is a better way to handle it but I just don't know how and probably I don't understand how the DataTime.Parse works!

   
// Manually parse the string to DateTime

string pattern = @"(?\d{4})-(?\d{2})-(?\d{2})T00:00:00";
System.Text.RegularExpressions.Match m = 
System.Text.RegularExpressions.Regex.Match(query.Element("joinDate").ToString(), pattern);
int year = int.Parse(m.Groups["year"].ToString());
int month = int.Parse(m.Groups["mth"].ToString());
int day = int.Parse(m.Groups["day"].ToString());

// this statement is also exactly how 
// I create the DateTime from the user's input.
DateTime d = new DateTime(year, month, day); 

Read XML from MemoryStream

This illustration of this example follows the previous post. What if we want to generate XML into a stream instead of a physical file? Initially, I thought the solution was very simple and could be done in a minute. Instead, it took me hours to figure out.

    public MemoryStream ToXml() {
      MemoryStream ms = new MemoryStream();
      
      XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
      ns.Add("", "");    

      XmlSerializer ser = new XmlSerializer(typeof(Subscribers));      
      ser.Serialize(ms, new Subscribers(), ns);       
 
      return ms;
    } 

If you read the XML from the MemoryStream returned by the above code with XDocument or XmlDocument, you will encounter "Root element is missing" error. For example,

  using (StreamReader reader = new StreamReader(new Subscribers().ToXml())) {
    subscribers = XDocument.Load(reader);
  }

When the XmlSerializer has finished the "write" into the MemoryStream, the stream pointer will be at the end of the XML structure. Thus, the function should rewind the pointer to the beginning of the stream before returning the MemoryStream to the caller. Using ms.Seek(0, SeekOrigin.Begin); . before the statement return ms; will do the trick.

    public MemoryStream ToXml() {
      MemoryStream ms = new MemoryStream();
      
      XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
      ns.Add("", "");    

      XmlSerializer ser = new XmlSerializer(typeof(Subscribers));      
      ser.Serialize(ms, new Subscribers(), ns);       
      
      ms.Seek(0, SeekOrigin.Begin);  // rewind the pointer the top of the stream

      return ms;
    } 

Serialization: How to override the element Name of an item inside a Collection

We cannot control the display name for the element if the object is an item inside a collection / array / list and etc. We can use XmlAttributeOverrides to override each property name of that object but not the element name of the object itself.

Consider the following scenerio of the XML structure: <customers><customer></customer>...<customer></customer>...</customers>

  <?xml version="1.0" encoding="utf-8"?>  
  <customers>
    <customer id="2600CD00">
      <firstName>Pat</firstName>
      <lastName>Thurston</lastName>
      ...
    </customer>
    <customer id="1E9CC4B0">
      <firstName>Kari</firstName>
      <lastName>Furse</lastName>
      ...
    </customer>
    <customer id="60R120B3">
      <firstName>Carl</firstName>
      <lastName>Stuart</lastName> 
      ...
    </customer>
    ...  
  </customers>

We want to make use of the existing classes to generate the above XML structure.

BeforeAfter
  public class Subscriber {
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string ID { get; set; }
    ...
  }
 [XmlRoot(ElementName = "customer")]
  public class Subscriber {
    [XmlElement(ElementName="firstName")]
    public string FirstName { get; set; }

    [XmlElement(ElementName="lastName")]
    public string LastName { get; set; }

    [XmlAttribute( AttributeName="id" )]
    public string ID { get; set; }
    
    ...
  }
  public class Subscribers 
    : System.ComponentModel.IListSource {

    System.ComponentModel.BindingList<Subscriber> bList;
    ...
    
  }
public class Subscribers 
  : System.ComponentModel.IListSource {

  System.ComponentModel.BindingList<Subscriber> bList;
  ...
  
  public void ToXml(string outputFileName) {   
    StreamWriter w = new StreamWriter(outputFileName); 
    
    XmlRootAttribute root 
     = new XmlRootAttribute("customers");        
      
    XmlSerializerNamespaces ns 
     = new XmlSerializerNamespaces();
    ns.Add("", "");    
    
    XmlSerializer ser 
     = XmlSerializer(bList.GetType(), root)    
    ser.Serialize(w, bList, ns); 
  }    
}    

Now when we call Subscribers to generate XML: new Subscribers().ToXml("customers.xml");, the result is not what we want.

  <?xml version="1.0" encoding="utf-8"?>  
  <customers>
    <Subscriber id="2600CD00">
      <firstName>Pat</firstName>
      <lastName>Thurston</lastName>
      ...
    </Subscriber>
    <Subscriber id="1E9CC4B0">
      <firstName>Kari</firstName>
      <lastName>Furse</lastName>
      ...
    </Subscriber>
    <Subscriber id="60R120B3">
      <firstName>Carl</firstName>
      <lastName>Stuart</lastName> 
      ...
    </Subscriber>
    ...  
  </customers>

If we directly serialize our Subscriber class, for sure, we can alter the element name at the root level:

  <?xml version="1.0" encoding="utf-8"?>    
    <customer id="AE19600F">
      <firstName>Janko</firstName>
      <lastName>Cajhen</lastName>
      ...
    </customer>

Inspired by my another personal project, I fortunately figured out my own solution.

The Solution:

  [XmlRoot("customers")]
  public class Subscribers 
    : System.ComponentModel.IListSource {

    System.ComponentModel.BindingList<Subscriber> bList;
    ...
    
    
    [XmlElement("customer")]
    public List<Subscriber> All {
      get {
        return bList.ToList();
      }
    }
    
    ...
    
    public void ToXml(string outputFileName) {
      StreamWriter w = new StreamWriter(outputFileName); 
        
      XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
      ns.Add("", "");    

      XmlSerializer ser = new XmlSerializer(typeof(Subscribers));      
      ser.Serialize(w, new Subscribers(), ns); 
    }    
  }

Using DataPager in ListView

If the DataSource is not known statically at design time, the DataPager may not work correctly. The following error could be expected to happen when you click on the link provided by DataPager at the second time:

Failed to load viewstate. The control tree into which viewstate is being loaded must match the control tree that was used to save viewstate during the previous request. For example, when adding controls dynamically, the controls added during a post-back must match the type and position of the controls added during the initial request.

This problem occurs because the DataPager has no idea how to perform or calculate paging for you without knowing what page is supposed to display (i.e., StartIndex, and MaximuumRows in the page) when the DataSource is only known at runtime. Thus, you need to provide this missing piece of information to the DataPager before databinding.

Under Google search, you may find that quite a few people implemented the PreRender event of DataPager to perform databinding. Unfortunately, it doesn't work for this scenario. You can bind the data at DataPager's PreRender event but you are unable to supply paging properties to DataPager as mentioned above. Both StartRowIndex and MaximumRows properties are needed to set for paging before databinding. This problem took me a few hours to resolve. It turns out that the solution is very simple.

The Solution: You should add and implement the PagePropertiesChanging event of ListView. The PagePropertiesChangingEventArgs from the event argument will provide all your needy paging properties (StartRowIndex and MaximumRows) so that you can supply them to the DataPager.

    protected void ListView1_PagePropertiesChanging(object sender, PagePropertiesChangingEventArgs e) {     
      this.DataPage1.SetPageProperties(e.StartRowIndex, e.MaximumRows, false);
      BindData();  // set DataSource to ListView and call DataBind() of ListView
    }

If the DataPager is placed inside the ListView, do this:

    protected void ListView1_PagePropertiesChanging(object sender, PagePropertiesChangingEventArgs e) {
      ListView lv = sender as ListView;
      DataPager pager = lv.FindControl("DataPage1") as DataPager;
      pager.SetPageProperties(e.StartRowIndex, e.MaximumRows, false);
      BindData();  // set DataSource to ListView and call DataBind() of ListView
    }

Sunday, April 10, 2011

StructureMap Configuration and Object Creation

Reference: StructureMap - Scoping and Lifecycle Management
My Test version: StructureMap 2.6.1

Configuration for Object Creation

Recently I've used StructureMap in one of my projects and I begin to like it, especially I can control the object life cycle via StructureMap without changing my code. The followings are some ways how to configure StructureMap to create object instance.

  1. Per request basis

    StructureMap by default constructs object instance transiently. Thus, each time you will get a new instance.

        public void NewPerRequest() {
    
          Container mymap = new Container(x => {
            x.For<ISimple>().Use<Simple>();
          });
    
          ISimple first = mymap.GetInstance<ISimple>();
          ISimple second = mymap.GetInstance<ISimple>();
    
          string formatter = "PerRequest [default]: instances are the same? {0}";
          System.Console.WriteLine(formatter, ReferenceEquals(first, second));
        }
    
  2. Singleton

    StructureMap can inject code for you to apply Singleton design pattern if you want your object remains one and only one instance to be active during the life of the application.

        public void Singleton() {
          Container mymap = new Container(x => {
            x.For<ISimple>().Singleton().Use<Simple>();
          });
    
          ISimple first = mymap.GetInstance<ISimple>();
          ISimple second = mymap.GetInstance<ISimple>();
    
          string formatter =  "Singleton: instances are the same? {0}";
          System.Console.WriteLine(formatter, ReferenceEquals(first, second));
        }    
    
  3. HttpContextScoped

    You can configure StructureMap to contruct objects to live in HttpContext scope. This method only uses in Web application.

        public void HttpContextScoped() {
          Container mymap = new Container(x => {
            x.For<ISimple>().HttpContextScoped().Use<Simple>();
          });
    
          using (new MockHttpContext()) {
            ISimple first = mymap.GetInstance<ISimple>();
            ISimple second = mymap.GetInstance<ISimple>();
    
            string formatter = "HttpContextScoped: instances are the same: {0}";
            System.Console.WriteLine(formatter, ReferenceEquals(first, second));
          }
        }
    
  4. HttpContextLifecycle

    Configure StructureMap to contruct objects to live in HttpContext life cycle. This method only uses in Web application.

        public void HttpContextLifecycle() {
          Container mymap = new Container(x => {
            x.For<ISimple>().LifecycleIs(new HttpContextLifecycle()).Use<Simple>();
          });
    
          using (new MockHttpContext()) {
            ISimple first = mymap.GetInstance<ISimple>();
            ISimple second = mymap.GetInstance<ISimple>();
    
            string formatter = "HttpContextLifecycle: instances are the same: {0}";
            System.Console.WriteLine(formatter, ReferenceEquals(first, second));
          }
        }
    
  5. HttpSessionLifecycle

    Configure StructureMap to instantiate an object in HttpSession context. Like the previous two methods, it only works in regular Web environment.

        public void HttpSessionLifecycle() {
          Container mymap = new Container(x => {
            x.For<ISimple>().LifecycleIs(new HttpSessionLifecycle()).Use<Simple>();
          });
    
    
          #region Create HttpSession environment for test
          // Mock a new HttpContext by using SimpleWorkerRequest
          System.Web.Hosting.SimpleWorkerRequest request =
            new System.Web.Hosting.SimpleWorkerRequest("/", string.Empty, string.Empty, string.Empty, new System.IO.StringWriter());
    
          System.Web.HttpContext.Current = new System.Web.HttpContext(request);
    
          MockHttpSession mySession = new MockHttpSession(
              Guid.NewGuid().ToString(),
              new MockSessionObjectList(),
              new System.Web.HttpStaticObjectsCollection(),
              600,
              true,
              System.Web.HttpCookieMode.AutoDetect,
              System.Web.SessionState.SessionStateMode.Custom,
              false);
    
          System.Web.SessionState.SessionStateUtility.AddHttpSessionStateToContext(
             System.Web.HttpContext.Current, mySession
          );
          #endregion
    
          // now we are ready to test
          ISimple first = mymap.GetInstance<ISimple>();
          ISimple second = mymap.GetInstance<ISimple>();
    
          string formatter = "HttpSessionLifecycle: instances are the same: {0}";
          System.Console.WriteLine(formatter, ReferenceEquals(first, second));
    
        }
    
  6. HybridSessionLifecycle

    Those related to HttpContext or HttpSession are only used in ASP.NET Web environment. You won't be able to have your instance to run in client or service environment. HybridSessionLifecycle may be used to resolve it. For this method, HybridSessionLifecycle will try to use HttpContext storage if it exists; otherwise, it uses ThreadLocal storage. However, I found that this method somehow won't work with HttpHandler.

        // According to my experience, 
        // HybridSessionLifecycle works in most situations (including regular Web environment, 
        // Web service, console) but it doesn't work well with HttpHandler while 
        // HybridHttpOrThreadLocalStorage does. 
        public void HybridSessionLifecycle() {
          Container mymap = new Container(x => {
            x.For<ISimple>().LifecycleIs(new HybridSessionLifecycle()).Use<Simple>();
          });
    
    
          ISimple first = mymap.GetInstance<ISimple>();
          ISimple second = mymap.GetInstance<ISimple>();
      
          string formatter = "HttpSessionLifecycle: instances are the same: {0}";
          System.Console.WriteLine(formatter, ReferenceEquals(first, second));
        }
    
  7. HybridHttpOrThreadLocalScoped

    Like HybridSessionLifecycle, it works for most situations. It also work well in a HttpHandler.

       // Unlike HybridSessionLifecycle, it works well with HttpHandlers and WCF Services 
        // besides a regular ASP.NET environment.
        public void HybridHttpOrThreadLocalScoped() {
          Container mymap = new Container(x => {        
            x.For<ISimple>().HybridHttpOrThreadLocalScoped().Use<Simple>();
          });
    
          ISimple first = mymap.GetInstance<ISimple>();
          ISimple second = mymap.GetInstance<ISimple>();
    
          string formatter = "HybridHttpOrThreadLocalScoped: instances are the same: {0}";
          System.Console.WriteLine(formatter, ReferenceEquals(first, second));
        }
    
  8. UniquePerRequestLifecycle

    You can instruct StructureMap to create an unique instance per request in order to ensure no corrupted data and all states in fresh.

        public void UniquePerRequestLifecycle() {
          Container mymap = new Container(x => {
            x.For<ISimple>().LifecycleIs(new UniquePerRequestLifecycle()).Use<Simple>();
          });
    
          ISimple first = mymap.GetInstance<ISimple>();
          ISimple second = mymap.GetInstance<ISimple>();
    
          string formatter = "HttpContext Scope: instances are the same: {0}";
          System.Console.WriteLine(formatter, ReferenceEquals(first, second));
        }
    

The Code

  • NUnit Test

    using System;
    using StructureMapTest.Mocks;
    using StructureMap;
    using StructureMap.Pipeline;
    using StructureMap.Configuration.DSL;
    using NUnit.Framework;
    
    namespace StructureMapTest.UnitTest {
      [TestFixture]
      public class ReferenceEqualTest {
    
        private delegate NUnit.Framework.Constraints.SameAsConstraint CompareConstraintDelegate(object expected);
    
        private void Compare<T>(string formatter, CompareConstraintDelegate sameOrNot) where T : ILifecycle {
          Container mymap = new Container(x => {
            T t = Activator.CreateInstance<T>();
            x.For<ISimple>().LifecycleIs(t).Use<Simple>();
          });
    
          Compare(mymap, formatter, sameOrNot);
        }
    
        private void Compare(Container mymap, string formatter, CompareConstraintDelegate sameOrNot) {
          ISimple first = mymap.GetInstance<ISimple>();
          ISimple second = mymap.GetInstance<ISimple>();
    
          System.Console.WriteLine(formatter, ReferenceEquals(first, second));
    
          // Simulate to 2 situations:
          // 1. Assert.That(first, Is.SameAs(second));
          // 2. Assert.That(first, Is.Not.SameAs(second));
          Assert.That(first, sameOrNot(second));
        }
    
        [Test]
        public void Different_on_per_request_basis() {
    
          Container mymap = new Container(x => {
            x.For<ISimple>().Use<Simple>();
          });
    
          Compare(mymap, "PerRequest [default]: instances are the same? {0}", Is.Not.SameAs);
        }
    
        [Test]
        public void Same_on_Singleton_instance() {
          Container mymap = new Container(x => {
            x.For<ISimple>().Singleton().Use<Simple>();
          });
    
          Compare(mymap, "Singleton: instances are the same? {0}", Is.SameAs);
        }
    
        [Test]
        public void Same_on_HttpContextScoped() {
          Container mymap = new Container(x => {
            x.For<ISimple>().HttpContextScoped().Use<Simple>();
          });
    
          using (new MockHttpContext()) {
            Compare(mymap, "HttpContextScoped: instances are the same: {0}", Is.SameAs);
          }
        }
    
        [Test]
        public void Same_on_HttpContextLifecycle() {
          Container mymap = new Container(x => {
            x.For<ISimple>().LifecycleIs(new HttpContextLifecycle()).Use<Simple>();
          });
    
          using (new MockHttpContext()) {
            Compare(mymap, "HttpContextLifecycle: instances are the same: {0}", Is.SameAs);
          }
        }
    
        [Test]
        public void Same_on_HttpSessionLifecycle() {
          Container mymap = new Container(x => {
            x.For<ISimple>().LifecycleIs(new HttpSessionLifecycle()).Use<Simple>();
          });
    
          #region Create HttpSession environment for test
          // Mock a new HttpContext by using SimpleWorkerRequest
          System.Web.Hosting.SimpleWorkerRequest request =
            new System.Web.Hosting.SimpleWorkerRequest("/", string.Empty, string.Empty, string.Empty, new System.IO.StringWriter());
    
          System.Web.HttpContext.Current = new System.Web.HttpContext(request);
    
          MockHttpSession mySession = new MockHttpSession(
              Guid.NewGuid().ToString(),
              new MockSessionObjectList(),
              new System.Web.HttpStaticObjectsCollection(),
              600,
              true,
              System.Web.HttpCookieMode.AutoDetect,
              System.Web.SessionState.SessionStateMode.Custom,
              false);
    
          System.Web.SessionState.SessionStateUtility.AddHttpSessionStateToContext(
             System.Web.HttpContext.Current, mySession
          );
          #endregion
    
          // now we are ready to test
          Compare(mymap, "HttpSessionLifecycle: instances are the same: {0}", Is.SameAs);
        }
    
        // According to my experience, 
        // HybridSessionLifecycle works in most situations (including regular Web environment, 
        // Web service, console) but it doesn't work well with HttpHandler while 
        // HybridHttpOrThreadLocalStorage does. 
        [Test]
        public void Same_on_HybridSessionLifecycle() {
          Compare<HybridSessionLifecycle>("HybridSessionLifecycle: instances are the same: {0}", Is.SameAs);
        }
    
        // Unlike HybridSessionLifecycle, it works well with HttpHandlers and WCF Services 
        // besides a regular ASP.NET environment.
        [Test]
        public void Same_on_HybridHttpOrThreadLocalScoped() {
          Container mymap = new Container(x => {
            //x.For<ISimple>().HybridHttpOrThreadLocalScoped().Use<Simple>().Named("MyInstanceName");
            x.For<ISimple>().HybridHttpOrThreadLocalScoped().Use<Simple>();
          });
          Compare(mymap, "HybridHttpOrThreadLocalScoped: instances are the same: {0}", Is.SameAs);
        }
    
        [Test]
        public void Different_on_UniquePerRequestLifecycle() {
          Compare<UniquePerRequestLifecycle>("HttpContext Scope: instances are the same: {0}", Is.Not.SameAs);
        }
    
      }
    }
    
  • class Simple

    using System;
    
    namespace StructureMapTest.Mocks {
      public interface ISimple {
        void DoSomething();
      }
    
      public class Simple : ISimple {
        public void DoSomething() {
        }
      }
    }
    
  • class MockHttpContext

    using System;
    
    namespace StructureMapTest.Mocks {
      public class MockHttpContext : IDisposable {
        private readonly System.IO.StringWriter sw;
    
        public MockHttpContext() {
          var httpRequest = new System.Web.HttpRequest("notExisted.aspx", "http://localhost", string.Empty);
          sw = new System.IO.StringWriter();
          var httpResponse = new System.Web.HttpResponse(sw);
          System.Web.HttpContext.Current = new System.Web.HttpContext(httpRequest, httpResponse);
        }
    
        public void Dispose() {
          sw.Dispose();
          System.Web.HttpContext.Current = null;
        }
      }
    }
    
  • class MockHttpSession and class MockSessionObjectList

    using System;
    
    namespace StructureMapTest.Mocks {
      // This class follows the example at:
      // http://msdn.microsoft.com/en-us/library/system.web.sessionstate.ihttpsessionstate.aspx
      public sealed class MockHttpSession : System.Web.SessionState.IHttpSessionState {
        ...
      } 
    } 
    
    using System;
    
    namespace StructureMapTest.Mocks {
      // This class follows the example at 
      // http://msdn.microsoft.com/en-us/library/system.web.sessionstate.isessionstateitemcollection(v=VS.90).aspx
      public class MockSessionObjectList : System.Web.SessionState.ISessionStateItemCollection {
        ...
      } 
    }  
    

Thursday, February 24, 2011

Why validators error messages not displayed in Google Chrome?

I asked myself several times. Why are validators error messages not displayed in Google Chrome? It is weird. Not a single validation error message is displayed in the page as soon as the following CSS is added. It works well with IE, Safari and Mozilla-type browsers but fails on Google Chrome.

  div.sink {padding:20px; width: 190px;border:1px silver solid;}
  div.sink input[type="text"]{width: 190px;}
  div.sink input[type="submit"]{width: 50px;}

I am sure that all the validators are fired and the page as a result reports invalid but there is no error message. There is no doubt that the problem is somehow related to the above CSS. To reduce the complexity of the page and easily to nail down the problem, I marked up a simple login page for investigation.

login page

With a bit modification, the page looks like this - very simple, only text boxes, RequiredFieldValidators and a login button. There is no code behind (no OnClick event) and they are straightly marks up.

<head>
<style type="text/css">

div.login {padding:20px; width: 190px; border:1px silver solid;}
div.login input[type="text"], div.login input[type="password"]{ width: 190px; }
div.login input[type="submit"]{width: 50px;}

</style>
</head>
<body>
<form id="form1" runat="server">
<div class="login">

  User Name:<br />
  <asp:TextBox id="txtUsername" runat="server" MaxLength="30"></asp:TextBox><br />     
  <asp:RequiredFieldValidator ID="RequiredFieldValidatorUsername" runat="server" 
       ControlToValidate="txtUsername" Display="Dynamic" 
       ErrorMessage="A username is required."></asp:RequiredFieldValidator><br />

  Password:<br />
  <asp:TextBox id="txtPassword" runat="server"  TextMode="Password" MaxLength="180" ></asp:TextBox><br />     
  <asp:RequiredFieldValidator ID="RequiredFieldValidatorPassword" runat="server" 
       ControlToValidate="txtPassword" Display="Dynamic" 
       ErrorMessage="A password is required."></asp:RequiredFieldValidator><br />

 <asp:Button ID="btnLogin" runat="server" Text="Log In" /><br />

</div>
</form>
</body>

With the above markup, the error message is shown at least for the User Name, unlike my problem page. The funny is that in this simple example, only the last validator won't be able to display error message if I keep adding more the similar control and validator. For sure, everything works normal if the CSS is removed.

No error message for the only and one validator

no error message for the only and one validator
No error message for the last validator
screen 1

no error message for the last validator - screen 1
No error message for the last validator
screen 2

no error message for the last validator - screen 2
No error message for the last validator
screen 3

no error message for the last validator - screen 3

The CSS is not complex. As a matter of face, it is very simple. What it does is to set the width for the input box or button in the page. To resolve this, I can simply add the width back to the control inself by removing the CSS. However, I cannot do it with my original page. Most controls are from user controls which won't allow me to set width. In addition, If I manage to add the width to them, the changes will affect other pages across the entire project unless I do it programically instead of static. It means I have to go in every single user control to add some code so that it can accept width change. I don't think that I want to go this route.

After playing around for some time, I finally found out the problem. The problem is the width setting in the first line.

  div.login {padding:20px; width: 190px; border:1px silver solid;}
  div.login input[type="text"], div.login input[type="password"]{ width: 190px; }
  div.login input[type="submit"]{width: 50px;}

Adding 4 more pixels fixes the problem!

  div.login {padding:20px; width: 194px; border:1px silver solid;}
  div.login input[type="text"], div.login input[type="password"]{ width: 190px; }
  div.login input[type="submit"]{width: 50px;}

It appears to me that Google Chrome browser cares about the outer width of the DIV when a CSS is specified. Most browsers may ignore it if the outer DIV is not width enough for its child components. From this instance, I learn that I need to pay attention to declare my CSS in order to prevent this from happening.

Saturday, February 12, 2011

Resource interpreted as image but transferred with MIME type text/html

One of my pages failed on Google Chrome. Because of the subject error, I could not debug it with Chrome debugger. The script won't be loaded into the debugger if the page is interpreted as an image. I wonder why and what caused it and I could not ignore it.

After hours looking into the code, I finally found the problem. In my case, I forget to include a JavaScript library reference in one of my user controls. As soon as I added it back, the error is gone and the page can be loaded into the Chrome debugger as other normal pages. Everything works fine.

Although the error message from Chrome is confusing, it at least alerts me that there is something wrong in my page. I am glad I found the problem and fixed it.

Why page events get called twice?

There are a lot of discussions about why the Page events get called twice or even multiple times. Most the solutions from Google search suggested to set AutoEventWireup to false. By default, it is true. Is it really helpful? For sure, a lot of people will disagree. Me either. The solution of AutoEventWireup doesn't stand for all cases. To me, most of the time, this issue is related to image resource.

Consider the following HTML code in the ASP.NET page markup.

<img src="" id="img1" alt="an image" style="display:none" />   cross

The HTML is valid but it is the trouble maker in ASP.NET Web form, which causes the page events being called twice because the src attribute is empty. Removing the src attribute entirely will fix the problem.

Let's consider another image related scenario. What if you want to set a background color in a table cell but mistakenly put background attribute instead of background-color?

<TD background="#008080">   cross

In this case, page events are also being called twice. But if HTML color name is used, everything will work fine. Thus the following HTML code won't cause trouble although it is not correct.

<TD background="Teal">    tickquestion mark

I am not quite sure how or when CalliHelper.EventArgFunctionCaller will bind the above scenarios to the page events. None of them are declared to run as server. But I do know that if the page events are being called twice, the first thing I will check is the markup, especially those HTML codes related to image resource.

Using From Target Attribute

[ Scenario ] [ The Solution ] [ Implemented with JavaScript ] [ Implemented with JQuery ] [ The Code ]

Problem Statement

A page may have several links and buttons to produce different forms of outout. Every single piece of data defined in the page including the input supplied by the user is needed for postback process. Is there a simple way to redirect the output in another window without overriding the current page so that the current page content stays perisistent?

Solution 1: Can we use target attribute of the element?

For a hyperlink, you may think using target attribute to accomplish this. With this approach, you have to gather all the needy input yourself from the current page into the querystring of the URL before posting data back to the server. This will introduce you another problem: querystring size limitation. Different plaforms and browser types have different limitations. Least to say, there is some work to be done in the client side. In addition, your server page must be configured and coded to handle HTTP GET to process the request.

For input button, we are out of luck. There isn't a target attribute defined in the specification. Thus, the action is completely relying on the form action.

Solution 2: How about AJAX?

Another approach to resolve this is using partial page rendering that would provide better user experience by eliminating the full page round trip overhead. Does it really matter in our case if we need every single piece of data defined or entered by the user in that page? It may save some overhead on some resources being loaded. Other than that, every data specified in that page is still posted back to the server for process. What we want is the output in another window so that the user may continue to use the same data to generate another form of output in a separate window and so on. Indeed, using AJAX is a bit complicated here. Of course, you may use some library like JQuery to set up your AJAX calls. For ASP.NET, you can simply accomplish this by using ScriptManager and UpdatePanel. Unfortunately, all of them require you to re-architect your page to accommodate the changes. To me, it is too much work. I need something simpler and make changes as least as possible in the page.

The Ultimate and Simple Solution: by using target attribute of the form.

The solution is to use the old trick defined in HTML with a little Javascript assistance.

In order to post data back to the server, we need a form tag. We usually don't specify the target attribute. In this case, the data is returned to the current page by default. If a target attribute is specified in the form tag, the output will be automatically routed to the specific frame or window by the browser when the data is returned by the server. For example,

Route data to a new window:

<form name="form1" id="form1" target="_blank" method="post" action="Default.aspx" onsubmit="..." >
...
</form>
</pre>

Route data to an iframe:

<form name="form1" id="form1" target="myIframe" method="post" action="Default.aspx" onsubmit="...">
...
</form>
<iframe name="myIframe" id="myIframe" width="400" height="300"></iframe>

Note that the name attribute of the iframe is required in this case. By specification, we should specify the_name_of_the_frame_or_window to the target attribute, not the_id_of_the_frame_or_window. But the id works for some Webkit type of browsers like Google Chrome.

With this simple solution in mind, I finally come up a way to unpuzzle the above problem with a very minor change in the page. The following is the solution presented in ASP.NET. The technique can be applied to elsewhere such as a simple HTML/CGI program.

What I need to do is dynamically adding a target attribute to the form before the data is being posted back to the server.

JavaScript Solution

First, let's define the script which does the injection. The JavaScript function may look like the following.

  function changeFormTarget() {
    var formObj = document.getElementById('form1');
    formObj.setAttribute('target', '_blank');
  }

What the above code does is, before posting data back to the server for process, it injects the target attribute into the form element by producing the following HTML code, which instructs the browser where to output the next document.

<form name="form1" id="form1" target="_blank" method="post" action="Default.aspx" onsubmit="...">

Then, we hook this function to the onclick event of the element before form submission. In ASP.NET, simply add OnClientClick event on the control to instruct the ASP.NET engine to process the client script first before postback.

<asp:LinkButton ID="lnkRptToNewTarget" runat="server" OnClientClick="changeFormTarget()" OnClick="RptToNewTarget_Click">Generate Report to New Target</asp:LinkButton><br />
<asp:Button ID="btnRptToNewTarget" runat="server" OnClientClick="changeFormTarget()" OnClick="RptToNewTarget_Click" Text="Generate Report to New Target"  />

The above markup will result in the following HTML code:

<a onclick="changeFormTarget();" id="lnkRptToNewTarget" 
   href="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("lnkRptToNewTarget", "", true, "", "", false, true))">Generate Report to New Target</a><br />
<input 
   type="submit" name="btnRptToNewTarget" id="btnRptToNewTarget"
   value="Generate Report to New Target" 
   onclick="changeFormTarget();WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("btnRptToNewTarget", "", true, "", "", false, false))" />

As you can see, with the very minimal changes in the page, a new window will be spawned off from the current page when the result is back.

JQuery Solution

If you're using JQuery, the solution is even simpler without changing any element in the page. Simply put the following script and then the onclick event will be automatically wired to the appropriate elements. In my example, simply ask JQuery to scan my elements and then wire them up with my supplied onclick event.

  $('#lnkRptToNewTarget,#btnRptToNewTarget').click(function() {
    $('form').attr('target', '_blank');
  });

With JQuery, basically the page remains intact without any markup or element being changed. The result is the same as JavaScript approach. Of course, you can write your own auto event wire up to achieve what JQuery does but there will be a lot of work.

Sometimes a simple solution works like a charm and it also eliminates the time to re-test and shortens the development time. I hope you will find this piece of information somehow useful.

The Code

If you want to test it yourself and see how this works, here is my simple backend event handler for output. In real life, the handler could be in another process which generates PDF or other non-HTML types of documents, and then call Response.Redirect() or Response.TransmitFile() to return the document to the client.

  protected void RptToNewTarget_Click(object sender, EventArgs e) {
    Response.Write(
      string.Format("Your report <b>{0}</b> is generated.", this.txtReportName.Text));
    Response.End();
  }

Here is the markup for JQuery approach. You can simply alter it to JavaScript solution that I discussed above.

<form id="form1" runat="server" defaultfocus="txtReportName">
<div>
  Enter Report Name: <br />
  <asp:TextBox ID="txtReportName" runat="server"></asp:TextBox>
  <asp:RequiredFieldValidator ID="RequiredFieldValidatorTxtReportName" runat="server" 
       ErrorMessage="Please enter the report name." Display="Dynamic" ControlToValidate="txtReportName"></asp:RequiredFieldValidator><br />
  <asp:LinkButton ID="lnkRptToNewTarget" runat="server" OnClick="RptToNewTarget_Click">Generate Report to New Target</asp:LinkButton><br />
  <asp:Button ID="btnRptToNewTarget" runat="server" OnClick="RptToNewTarget_Click" Text="Generate Report to New Target"  />
</div>
</form>

<script type="text/javascript">
  $('#lnkRptToNewTarget,#btnRptToNewTarget').click(function() {
    $('form').attr('target', '_blank');
  });
</script>

Friday, August 6, 2010

I'm certified - MCTS

keep smiling

Today is my lucky and happy day. I finally got certified with ASP.NET 3.5. I was preparing it for the past few months. Today I finally get an award of it.

My exam time was scheduled at 11:30AM today. I left and headed to the test center an hour ahead of my exam time. It was just about 10 minutes drive but I was almost late and missed the time because of the incorrect Google map. I kept calling the test center but I could not reach anyone. I was panic and even thought that I might be doomed today. The time was ticking and I had only 10 minutes left. Finally, my call got through and I arrived just right on time. Still, my feeling was not so good that I was afraid what happened before was to give me some sort of symbolic or indication that I could fail my exam. However, the result turns out fantastic (a perfect one) and it makes me keep smiling.

The actual exam time lasts 180 minutes (3 hours) for 45 questions. But my confirmation sheet (from Prometric registration) said 4 hours because the additional hour was used for reading instructions, answering questions for survey (compulsorily) and leaving comments to questions after test (voluntarily). When I started taking the exam, the time was set to 180 and then counted down. I spent about 2 hours and read twice. The full score is 1000 and the passing mark is 700.

Saturday, June 12, 2010

The LoadDataRow and Delete Methods of DataTable

Oftentimes we encounter problems when we perform update using LoadDataRow or rollback data using RejectChanges after the Delete method. Why do the methods sometimes not do what we want?

Googling a solution for LoadDataRow, we may find that most people resolve the update performed by LoadDataRow must call DataTable.AcceptChanges first. Does this solution work for you 100%? There are still some unresolved posts left unanswered. They follow the solution to call AcceptChanges but the problem still exists. Why?

Deleting a DataRow and then immediately performing RejectAccepts to rollback cause us to receive System.Data.RowNotInTableException. What is it going on? Is the DataRow marked as Deleted? Why can't I rollback?

The above problems are related to either the RowState of a DataRow or if we set up the constraints on DataTable correctly.

continue Assuming there is no constraint problem, the DataRow must be either Unchanged or Modified when we perform an update using the LoadDataRow method on an existing DataRow. The same principle applies to the RejectChanges after calling the Delete method. Apparently, the operations requires an Original version existed for the DataRow. When the RowState of DataRow is Unchanged or Modified, an Original version of the DataRow has been created/existed. Thus, any operations after it won't cause a problem.

continueIf you receive a System.Data.ConstraintException in regard to System.Data.UniqueConstraint.CheckConstraint, please ensure the primary key set up properly. Most likely, setting the Unique property of a DataColumn without primary key to true will cause this problem to occur.

continueIf you receive a System.Data.ConstraintException in regard to System.Data.DataTable.EnableConstraints, please ensure an Original version existed for the DataRow. It means the RowState of the DataRow is still in the state of Added and you need to commit changes first by calling the AcceptChanges method to update the state to Unchanged.

continueSimilarly, if you receive a System.Data.RowNotInTableException when you try to roll back the DataRow after having called the Delete method, a AcceptChanges call is required before the Delete method. After AcceptChanges, an Original version of the DataRow is created and the RowState of the DataRow is set to Unchanged. From this state onwards, any modification including the LoadDataRow method, or calling the Delete method and then the RejectChanges method can be performed.

To sum up what the RowState means is described in the following table.
DetachedThe DataRow is created but it hasn't been added to a DataTable yet. Since it does not exist in a DataTable, no operations can be performed at the DataTable level.
AddedThe DataRow is added to a DataTable but the DataTable has not accepted changes yet. At this stage, it requires AcceptChanges to create Original version for other update operations including rollback.
UnchangedThe DataRow has not changed since the last AcceptChanges call. At this stage, an Original version of the DataRow has already existed. Thus, any operations can be performed without a problem.
ModifiedThe DataRow has been modified since the last AcceptChanges call. Like Unchanged, an Original version of the DataRow has already existed in this stage. A DataRow with Modified row state can continously be modified but the RejectChanges will only roll the data back to the last AcceptChanges call. Any modification made in between is disgarded because the Original version of the DataRow is only pointing the copy when the last AcceptChanges is called.
DeletedThe DataRow was deleted by using the Delete method of the DataRow only when the RowState of the DataRow is Unchanged or Modified.

I also attach some test results I conducted for reference.

Friday, June 11, 2010

About DataRow.Delete, .AcceptChanges and .RejectChanges

See The LoadDataRow and Delete Methods of DataTable for intention.

Purpose

The following 3 tests [ Test 1 | Test 2 | Test 3 ] tell you about in which situation you can roll back your data and when you cannot.

This page is generated by the application developed in C# under Visual 2008 Profession SP + .NET 3.5 SP1 on Windows XP Professional SP3.

Test 1: No AcceptChanges before deletion
  • Set up a DataTable with 2 rows; so the initial RowState of all DataRows is Added.
  • Without AcceptChanges, immediately delete the 2nd DataRow using Delete method.
  • The RowState of the deleted row is not Deleted but Detached; The result is different from what it is said in the documentation [According to MSDN, it is currently supported in: 4, 3.5, 3.0, 2.0, 1.1, 1.0]. Please also see the test foot notes in blue.
  • Then RejectChanges after deletion; unfortunately, the System.Data.RowNotInTableException is received.

EmpIdFirstNameLastNameSalaryLastName and FirstNameRowState
A123456789BettyeWilliams11.00Williams, BettyeAdded
B223456789RexBloomer12.00Bloomer, RexAdded

Initial state: [B223456789, Rex] RowState: Added
Version: Original doesn't not exist.
Version: Current Value: Rex
Version: Proposed doesn't not exist.
Version: Default Value: Rex

Using Delete method to delete [B223456789, Rex] at RowState: Added...
EmpIdFirstNameLastNameSalaryLastName and FirstNameRowState
A123456789BettyeWilliams11.00Williams, BettyeAdded

The deleted row after deletion: RowState: Detached
Version: Original doesn't not exist.
Version: Current doesn't not exist.
Version: Proposed doesn't not exist.
Version: Default doesn't not exist.

This row has been removed from a table and does not have any data.  BeginEdit() will allow creation of new data in this row.
System.Data.RowNotInTableException
at System.Data.DataRow.GetDefaultRecord() at System.Data.DataColumn.CheckNullable(DataRow row) at System.Data.DataColumn.CheckColumnConstraint(DataRow row, DataRowAction action) at System.Data.DataTable.RaiseRowChanging(DataRowChangeEventArgs args, DataRow eRow, DataRowAction eAction, Boolean fireEvent) at System.Data.DataTable.SetNewRecordWorker(DataRow row, Int32 proposedRecord, DataRowAction action, Boolean isInMerge, Int32 position, Boolean fireEvent, Exception& deferredException) at System.Data.DataTable.SetNewRecord(DataRow row, Int32 proposedRecord, DataRowAction action, Boolean isInMerge, Boolean fireEvent) at System.Data.DataTable.RollbackRow(DataRow row) at System.Data.DataRow.RejectChanges() ...

After error: RowState: Detached
Version: Original doesn't not exist.
Version: Current doesn't not exist.
Version: Proposed doesn't not exist.
Version: Default doesn't not exist.

  • By definition of DataRow.Delete method, the row should have been marked as Deleted.
  • According to this, this bug should have been fixed in VS 2008. Apparently, it still exists. Or there may be a typo in the document. Originally it said, "If the RowState of the row is Added, the RowState becomes Detached and the row is removed from the table when you call AcceptChanges." Perhaps, it is meant to say, ""If the RowState of the row is Added, the RowState becomes Detached and the row is removed from the table when you call RejectChanges." Then everything will become and make more sense.
  • Because DataRow.RowState is set to Detached, we cannot roll it back by using RejectChanges.


Test 2: AcceptChanges before deletion
  • Set up a DataTable with 2 rows; so the initial RowState of all DataRows is Added.
  • Then AcceptChanges, which changes the RowState from Added to Unchanged.
  • Delete the 2nd DataRow using Delete method. The RowState of the 2nd DataRow is marked as Deleted now.
  • Immdiately RejectChanges after deletion, which changes the RowState from Deleted back to Unchanged

EmpIdFirstNameLastNameSalaryLastName and FirstNameRowState
A123456789BettyeWilliams11.00Williams, BettyeAdded
B223456789RexBloomer12.00Bloomer, RexAdded

Initial state: [B223456789, Rex] RowState: Added
Version: Original doesn't not exist.
Version: Current Value: Rex
Version: Proposed doesn't not exist.
Version: Default Value: Rex

AcceptChanges. [B223456789, Rex] RowState: Unchanged
Version: Original Value: Rex
Version: Current Value: Rex
Version: Proposed doesn't not exist.
Version: Default Value: Rex

Using Delete method to delete [B223456789, Rex] at RowState: Unchanged...
EmpIdFirstNameLastNameSalaryLastName and FirstNameRowState
A123456789BettyeWilliams11.00Williams, BettyeUnchanged
?????Deleted

The deleted row after deletion: RowState: Deleted
Version: Original Value: Rex
Version: Current doesn't not exist.
Version: Proposed doesn't not exist.
Version: Default doesn't not exist.

RejectChanges:
EmpIdFirstNameLastNameSalaryLastName and FirstNameRowState
A123456789BettyeWilliams11.00Williams, BettyeUnchanged
B223456789RexBloomer12.00Bloomer, RexUnchanged

[B223456789, Rex] RowState: Unchanged
Version: Original Value: Rex
Version: Current Value: Rex
Version: Proposed doesn't not exist.
Version: Default Value: Rex

  • After deletion using Delete method, the RowState is set to Deleted.
  • Thus, RejectChanges can be used to roll back the DataRow to the point where the AcceptChanges method is last called.
  • The RowState of the deleted row is also rolled back to Unchanged. Everything looks exactly like before deletion.

Test 3: AcceptChanges, modification and then deletion
  • Set up a DataTable with 2 rows; so the initial RowState of all DataRows is Added.
  • Then AcceptChanges, which changes the RowState from Added to Unchanged.
  • Start BeginEdit ... EndEdit block to modify the 2nd row. Change the FirstName from Rex to Susan.
  • The expression column LastName and FirstName remains unchanged until the EndEdit method is called while FirstName is modified before calling EndEdit.
  • Then delete the 2nd DataRow using Delete method. The RowState of the 2nd DataRow is marked as Deleted now.
  • Immdiately RejectChanges after deletion, which changes everything back to the state when the AcceptChanges is last called; thus the RowState is rolled back all the way from Deleted to Unchanged

EmpIdFirstNameLastNameSalaryLastName and FirstNameRowState
A123456789BettyeWilliams11.00Williams, BettyeAdded
B223456789RexBloomer12.00Bloomer, RexAdded

Initial state: [B223456789, Rex] RowState: Added
Version: Original doesn't not exist.
Version: Current Value: Rex
Version: Proposed doesn't not exist.
Version: Default Value: Rex

AcceptChanges. [B223456789, Rex] RowState: Unchanged
Version: Original Value: Rex
Version: Current Value: Rex
Version: Proposed doesn't not exist.
Version: Default Value: Rex

BeginEdit and modified the 2nd row:
 row.BeginEdit(); row["FirstName"] = "Susan";

EmpIdFirstNameLastNameSalaryLastName and FirstNameRowState
A123456789BettyeWilliams11.00Williams, BettyeUnchanged
B223456789SusanBloomer12.00Bloomer, RexUnchanged
[B223456789, Susan] RowState: Unchanged
Version: Original Value: Rex
Version: Current Value: Rex
Version: Proposed Value: Susan
Version: Default Value: Susan

After EndEdit:
  row.EndEdit();

EmpIdFirstNameLastNameSalaryLastName and FirstNameRowState
A123456789BettyeWilliams11.00Williams, BettyeUnchanged
B223456789SusanBloomer12.00Bloomer, SusanModified
[B223456789, Susan] RowState: Modified
Version: Original Value: Rex
Version: Current Value: Susan
Version: Proposed doesn't not exist.
Version: Default Value: Susan

Using Delete method to delete [B223456789, Susan] at RowState: Modified...
EmpIdFirstNameLastNameSalaryLastName and FirstNameRowState
A123456789BettyeWilliams11.00Williams, BettyeUnchanged
?????Deleted

The deleted row after deletion: RowState: Deleted
Version: Original Value: Rex
Version: Current doesn't not exist.
Version: Proposed doesn't not exist.
Version: Default doesn't not exist.

RejectChanges:
EmpIdFirstNameLastNameSalaryLastName and FirstNameRowState
A123456789BettyeWilliams11.00Williams, BettyeUnchanged
B223456789RexBloomer12.00Bloomer, RexUnchanged

[B223456789, Rex] RowState: Unchanged
Version: Original Value: Rex
Version: Current Value: Rex
Version: Proposed doesn't not exist.
Version: Default Value: Rex

Interesting of this test is that the "LastName and FirstName" column remains unchanged [Bloomer, Rex] after the FirstName has been modified to Susan. The "LastName and FirstName" column is an Expression column set up by expression that contains the values from both FirstName and LastName columns. Changing FirstName will not refect the change immediately in the "FirstName and LastName" column until EndEdit is called.

Wednesday, June 9, 2010

LoadDataRow(object[], bool) and ConstraintException

See The LoadDataRow and Delete Methods of DataTable for intention.


Introduction

In this test, I will use LoadDataRow to perform content update on a DataTable with or without primary key constraint. bool AcceptChanges will be passed as a parameter in LoadDataRow(object[], bool). For result using LoadDataRow(object[], LoadOption), please see this.

When the LoadDataRow method is call, the following statement is used:
   employee.LoadDataRow(
      new object[] { "B223456789", "Denise", "Miller", 13.00 },
      false);  // don't AcceptChanges
When primary key is applied, the following statement is used and it will be executed before the LoadDataRow method:
   employee.PrimaryKey = new DataColumn[] {eid}; // eid is the EmpId column   

The DataTable used for the test has the five (5) columns and initially is populated with 2 sample data rows.
ColumnNameEmpIdFirstNameLastNameSalaryLastName and FirstName **
DataTypestringstringstringdecimalstring
MaxLength1035-1-170
Uniquetruefalsefalsefalsefalse
AllowDBNullfalsefalsefalsetruetrue

** The column of LastName and FirstName is defined as an expression column containing the values from the column of LastName and the column of FirstName.

Every test is based on this table with the initial sample data. The 2nd row is the target to update by using the LoadDataRow method. A grid shown in each test is represented the final result of the DataTable even though the update performed by the LoadDataRow method may fail. Execution messages about the RowState of the 2nd row and its versions are shown afterwards. For clarity, the initial state of the 2nd row is also attached to each test for reference.

The rest of the page is generated by the application developed in C# under Visual 2008 Profession SP + .NET 3.5 SP1 on Windows XP Professional SP3.


Set up a DataTable with 2 rows
- EmpId is the unique key (eid.Unique = true).
- Primary key is not set up initially.

  • Initial Table:
  • EmpIdFirstNameLastNameSalaryLastName and FirstName
    A123456789BettyeWilliams11.00Williams, Bettye
    B223456789RexBloomer12.00Bloomer, Rex

  • Status duing execution:
  • Initial state: [B223456789, Rex] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex


No primary key applied
Expect errors [System.Data.UniqueConstraint.CheckConstraint]!
The last row remains unchanged.

  • Resulted DataTable:
  • EmpIdFirstNameLastNameSalaryLastName and FirstName
    A123456789BettyeWilliams11.00Williams, Bettye
    B223456789RexBloomer12.00Bloomer, Rex

  • Status duing execution:
  • Initial state: [B223456789, Rex] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex

    Executing LoadDataRow(...)
    Column 'EmpId' is constrained to be unique.  Value 'B223456789' is already present.
    System.Data.ConstraintException
    at System.Data.UniqueConstraint.CheckConstraint(DataRow row, DataRowAction action) at System.Data.DataTable.RaiseRowChanging(DataRowChangeEventArgs args, DataRow eRow, DataRowAction eAction, Boolean fireEvent) at System.Data.DataTable.SetNewRecordWorker(DataRow row, Int32 proposedRecord, DataRowAction action, Boolean isInMerge, Int32 position, Boolean fireEvent, Exception& deferredException) at System.Data.DataTable.InsertRow(DataRow row, Int32 proposedID, Int32 pos, Boolean fireEvent) at System.Data.DataRowCollection.Add(Object[] values) at System.Data.DataTable.UpdatingAdd(Object[] values) at System.Data.DataTable.LoadDataRow(Object[] values, Boolean fAcceptChanges) ...

    After error: [B223456789, Rex] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex


Primary key applied, no AcceptChanges before or after LoadDataRow
Expect errors [System.Data.UniqueConstraint.CheckConstraint]!
The last row remains unchanged.

  • Resulted DataTable:
  • EmpIdFirstNameLastNameSalaryLastName and FirstName
    A123456789BettyeWilliams11.00Williams, Bettye
    B223456789RexBloomer12.00Bloomer, Rex

  • Status duing execution:
  • Initial state: [B223456789, Rex] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex

    Executing LoadDataRow(...)
    Column 'EmpId' is constrained to be unique.  Value 'B223456789' is already present.
    System.Data.ConstraintException
    at System.Data.UniqueConstraint.CheckConstraint(DataRow row, DataRowAction action) at System.Data.DataTable.RaiseRowChanging(DataRowChangeEventArgs args, DataRow eRow, DataRowAction eAction, Boolean fireEvent) at System.Data.DataTable.SetNewRecordWorker(DataRow row, Int32 proposedRecord, DataRowAction action, Boolean isInMerge, Int32 position, Boolean fireEvent, Exception& deferredException) at System.Data.DataTable.InsertRow(DataRow row, Int32 proposedID, Int32 pos, Boolean fireEvent) at System.Data.DataTable.UpdatingAdd(Object[] values) at System.Data.DataTable.LoadDataRow(Object[] values, Boolean fAcceptChanges) ...

    After error: [B223456789, Rex] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex


No primary key, AcceptChanges before LoadDataRow
Expect errors [System.Data.UniqueConstraint.CheckConstraint]!
The last row remains unchanged.

  • Resulted DataTable:
  • EmpIdFirstNameLastNameSalaryLastName and FirstName
    A123456789BettyeWilliams11.00Williams, Bettye
    B223456789RexBloomer12.00Bloomer, Rex

  • Status duing execution:
  • Initial state: [B223456789, Rex] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex

    Apply AcceptChanges(): [B223456789, Rex] RowState: Unchanged
    Version: Original Value: Rex
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex

    Executing LoadDataRow(...)
    Column 'EmpId' is constrained to be unique.  Value 'B223456789' is already present.
    System.Data.ConstraintException
    at System.Data.UniqueConstraint.CheckConstraint(DataRow row, DataRowAction action) at System.Data.DataTable.RaiseRowChanging(DataRowChangeEventArgs args, DataRow eRow, DataRowAction eAction, Boolean fireEvent) at System.Data.DataTable.SetNewRecordWorker(DataRow row, Int32 proposedRecord, DataRowAction action, Boolean isInMerge, Int32 position, Boolean fireEvent, Exception& deferredException) at System.Data.DataTable.InsertRow(DataRow row, Int32 proposedID, Int32 pos, Boolean fireEvent) at System.Data.DataRowCollection.Add(Object[] values) at System.Data.DataTable.UpdatingAdd(Object[] values) at System.Data.DataTable.LoadDataRow(Object[] values, Boolean fAcceptChanges) ...

    After error: [B223456789, Rex] RowState: Unchanged
    Version: Original Value: Rex
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex


Primary key applied, AcceptChanges before LoadDataRow
No error! The last row is updated.

  • Resulted DataTable:
  • EmpIdFirstNameLastNameSalaryLastName and FirstName
    A123456789BettyeWilliams11.00Williams, Bettye
    B223456789DeniseMiller13.00Miller, Denise

  • Status duing execution:
  • Initial state: [B223456789, Rex] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex

    Apply AcceptChanges(): [B223456789, Rex] RowState: Unchanged
    Version: Original Value: Rex
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex

    Executing LoadDataRow(...)
    After LoadDataRow: [B223456789, Denise] RowState: Modified
    Version: Original Value: Rex
    Version: Current Value: Denise
    Version: Proposed doesn't not exist.
    Version: Default Value: Denise


No primary key, LoadDataRow in conjunction with Begin/EndLoadData
Expect errors [System.Data.DataTable.EnableConstraints()]!
Duplicate rows are found.

  • Resulted DataTable:
  • EmpIdFirstNameLastNameSalaryLastName and FirstName
    A123456789BettyeWilliams11.00Williams, Bettye
    B223456789RexBloomer12.00Bloomer, Rex
    B223456789DeniseMiller13.00Miller, Denise

  • Status duing execution:
  • Initial state: [B223456789, Rex] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex

    BeginLoadData(): turn off notification/constraints/indexing.

    Executing LoadDataRow(...)
    After LoadDataRow: [B223456789, Denise] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Denise
    Version: Proposed doesn't not exist.
    Version: Default Value: Denise

    EndLoadData(): turn on validation, constraints, etc.

    Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints.
    System.Data.ConstraintException
    at System.Data.DataTable.EnableConstraints() at System.Data.DataTable.set_EnforceConstraints(Boolean value) at System.Data.DataTable.EndLoadData() ...

    After error: [B223456789, Rex] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex


Primary key applied, LoadDataRow in conjunction with Begin/EndLoadData
Expect errors [System.Data.DataTable.EnableConstraints()]!
Duplicate rows are found.

  • Resulted DataTable:
  • EmpIdFirstNameLastNameSalaryLastName and FirstName
    A123456789BettyeWilliams11.00Williams, Bettye
    B223456789RexBloomer12.00Bloomer, Rex
    B223456789DeniseMiller13.00Miller, Denise

  • Status duing execution:
  • Initial state: [B223456789, Rex] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex

    BeginLoadData(): turn off notification/constraints/indexing.

    Executing LoadDataRow(...)
    After LoadDataRow: [B223456789, Denise] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Denise
    Version: Proposed doesn't not exist.
    Version: Default Value: Denise

    EndLoadData(): turn on validation, constraints, etc.

    Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints.
    System.Data.ConstraintException
    at System.Data.DataTable.EnableConstraints() at System.Data.DataTable.set_EnforceConstraints(Boolean value) at System.Data.DataTable.EndLoadData() ...

    After error: [B223456789, Rex] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex


Primary key applied, LoadDataRow in conjunction with Begin/EndLoadData, ApplyChanges before BeginLoadData
No error! The last row is updated.

  • Resulted DataTable:
  • EmpIdFirstNameLastNameSalaryLastName and FirstName
    A123456789BettyeWilliams11.00Williams, Bettye
    B223456789DeniseMiller13.00Miller, Denise

  • Status duing execution:
  • Initial state: [B223456789, Rex] RowState: Added
    Version: Original doesn't not exist.
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex

    Apply AcceptChanges(): [B223456789, Rex] RowState: Unchanged
    Version: Original Value: Rex
    Version: Current Value: Rex
    Version: Proposed doesn't not exist.
    Version: Default Value: Rex

    BeginLoadData(): turn off notification/constraints/indexing.

    Executing LoadDataRow(...)
    After LoadDataRow: [B223456789, Denise] RowState: Modified
    Version: Original Value: Rex
    Version: Current Value: Denise
    Version: Proposed doesn't not exist.
    Version: Default Value: Denise

    EndLoadData(): turn on validation, constraints, etc.

    [B223456789, Denise] RowState: Modified
    Version: Original Value: Rex
    Version: Current Value: Denise
    Version: Proposed doesn't not exist.
    Version: Default Value: Denise