Blogs

The One With

December 2008 - Posts

  • Implementing a URL Shrinker

         

    It seems that the length of URLs gets bigger and bigger every hour.

    Here is a Google Maps URL:

    http://www.google.com/maps?f=q&hl=en&geocode=&q=805+N.Brand+Glendale,+CA+91210&sll=37.0625,-95.677068&sspn=25.704151,53.789062&ie=UTF8&ll=34.154609,-118.245227&spn=0.017419,0.033045&z=16&g=805+N.Brand+Glendale,+CA+91210&iwloc=addr&layer=c&cbll=34.158046,-118.255165&panoid=O08NNpO077m501EOlXSgUw&cbp=12,237.8462640496058,,0,-1.0291286092171708

    Ugly isn't?

    Here is another one I grabbed from a SharePoint site:

    http://fiji/Shared%20Documents/Forms/AllItems.aspx?RootFolder=/Shared%20Documents/Specifications/Silverlight/AgDataGrid&FolderCTID=&View={8405395A-43B7-4666-BE45-CEBCE564CE88}

    Not as bad as the Google Maps' one but unreadable nonetheless. And I am sure a lot of you have seen some that are even longer then that.

    There are services out there that can help with that. http://TinyURL.com is probably the most popular one. Unfortunately, public services like that cannot be used by most companies if the URL in question is an internal one. A URL shrinking service must reside on the corp net. So let's quickly write one.

    Getting Started

    I am going to start with a new Silverlight application and have an ASP.NET Web Project generated for me by the Visual Studio wizard. We'll concentrate on the server side of things first. We will need:

    • A database to store our URL to ID mappings
    • A page that will handle the redirection
    • And some UI that can take in a long URL and shrink it.

    To store the URLs, I am going to use XPO. (MS Access as a back-end during development and something more mature like SQL Server for production)

    The shrinker requires only one table:

       1: namespace Shrinker.Data {
       2:     [OptimisticLocking(false)]
       3:     public class Link : PersistentBase {    
       4:         public Link(Session session)
       5:             : base(session) {
       6:         }
       7:         
       8:         int _id;
       9:         [Key(AutoGenerate = true)]
      10:         public int ID {
      11:             get { return _id; }
      12:             set { SetPropertyValue("ID", ref _id, value); }
      13:         }
      14:  
      15:         string _url;
      16:         [Size(1024)]
      17:         public string Url {
      18:             get { return _url; }
      19:             set { SetPropertyValue("Url", ref _url, value); }
      20:         }
      21:     }
      22: }
    and two routines, Get and Put.
       1: namespace Shrinker {
       2:     public static class Utils {
       3:         static DevExpress.Xpo.DB.IDataStore _store;    
       4:  
       5:         static Utils() {
       6:             string connectionString = DevExpress.Xpo.DB.AccessConnectionProvider.GetConnectionString(
       7:                 HttpContext.Current.ApplicationInstance.Server.MapPath("~\\App_Data\\Shrinker.mdb"));
       8:  
       9:             _store = DevExpress.Xpo.XpoDefault.GetConnectionProvider(connectionString,
      10:                 DevExpress.Xpo.DB.AutoCreateOption.DatabaseAndSchema);
      11:         }
      12:  
      13:         public static int Put(Uri uri) {
      14:             using (UnitOfWork unitOfWork = new UnitOfWork(new SimpleDataLayer(_store))) {
      15:                 var link = unitOfWork.FindObject<Data.Link>(new BinaryOperator("Url", uri.AbsoluteUri));
      16:                 if (link != null && link.ID > 0) {
      17:                     return link.ID;
      18:                 }
      19:  
      20:                 link = new Data.Link(unitOfWork);
      21:                 link.Url = uri.AbsoluteUri;
      22:  
      23:                 unitOfWork.Save(link);
      24:                 unitOfWork.CommitChanges();
      25:  
      26:                 return link.ID;
      27:             };
      28:         }
      29:  
      30:         public static string Get(int id) {
      31:             using (UnitOfWork unitOfWork = new UnitOfWork(new SimpleDataLayer(_store))) {
      32:                 var link = unitOfWork.FindObject<Data.Link>(new BinaryOperator("ID", id));                
      33:                 if (link != null && link.ID > 0) {
      34:                     return link.Url;
      35:                 }
      36:             };
      37:             return null;
      38:         }
      39:  
      40:     }
      41: }
    we cab use this now in our Default.aspx. Simply check if there are any params passed, if so try to grab the actual URL, otherwise navigate to a UI page.  (Input validation and error checking omitted for clarity)
       1: protected void Page_Load(object sender, EventArgs e) {
       2:     if (Request.QueryString.Count == 0) {
       3:         Response.Redirect("Shrinker.aspx", true);
       4:         return;
       5:     }
       6:     Response.Redirect(Utils.Get(int.Parse(Request.QueryString[0])));
       7: }

    User Interface

    For the UI, let's create a simple ASP.NET page with ASPxRoundPanel, ASPxTextBox and an ASPxButton on it.

    (I customized it using the BlackGlass Theme)

    and in the Page_Load simply use Utils.Put to store the URL from the text box.

       1: namespace Shrinker.Web {
       2:     public partial class Shrinker : System.Web.UI.Page {
       3:         protected void Page_Load(object sender, EventArgs e) {
       4:             if (IsPostBack || IsCallback) {
       5:                 try {
       6:                     Uri uri = new Uri(urlBox.Text);
       7:                     errorLabel.Visible = false;
       8:                     linkLabel.NavigateUrl = string.Format(ConfigurationSettings.AppSettings["RedirectUrl"], Utils.Put(uri).ToString());
       9:                     linkLabel.Text = linkLabel.NavigateUrl;
      10:                     linkLabel.Visible = true;
      11:                 }
      12:                 catch (Exception error) {
      13:                     linkLabel.Visible = false;
      14:                     errorLabel.Text = error.ToString();
      15:                     errorLabel.Visible = true;
      16:                 }
      17:             }
      18:         }
      19:     }
      20: }
    we'll add the RedirectUrl to Web.config so that we can change on the production server.
       1: <appSettings>
       2:     <add key="RedirectUrl" value="http://localhost:8081?{0}" />
       3: </appSettings>
    
    
    and that's it.
     

    Silverlight Client

    For the Silverlight Client, we will expose the Utils.Put as a WCF web service.

       1: namespace Shrinker.Web {
       2:     [ServiceContract(Namespace = "")]
       3:     [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
       4:     public class WebService {
       5:         [OperationContract]
       6:         public string Put(string uriString) {
       7:             return string.Format(
       8:                 ConfigurationSettings.AppSettings["RedirectUrl"],
       9:                 Utils.Put(new Uri(uriString)).ToString());
      10:         }
      11:     }
      12: }
    import it into our Silverlight Project and call it like so:
       1: namespace Shrinker {
       2:     public partial class Page : UserControl {
       3:         private void OnClick(object sender, RoutedEventArgs e) {
       4:             WebService.WebServiceClient client = new Shrinker.WebService.WebServiceClient();
       5:             client.PutCompleted += new EventHandler<Shrinker.WebService.PutCompletedEventArgs>(PutCompleted);
       6:             client.PutAsync(urlBox.Text);
       7:         }
       8:  
       9:         void PutCompleted(object sender, Shrinker.WebService.PutCompletedEventArgs e) {
      10:             if (e.Error == null) {
      11:                 linkBox.Text = e.Result;
      12:             }
      13:             else {
      14:                 linkBox.Text = e.Error.ToString();
      15:             }
      16:         }
      17:     }
      18: }
    The layout for the Page.xaml is done using the AgLayoutControl
       1: <layout:LayoutControl Background="#C9D7DC" VerticalAlignment="Top" Margin="0,32,0,0"  Orientation="Vertical">
       2:             <layout:LayoutGroup VerticalAlignment="Top" HorizontalAlignment="Stretch" Orientation="Horizontal">
       3:                 <TextBlock Text="URL:" HorizontalAlignment="Left" VerticalAlignment="Center"></TextBlock>
       4:                 <TextBox x:Name="urlBox" HorizontalAlignment="Stretch" Text="http://"></TextBox>
       5:                 <Button Content="Shrink" HorizontalAlignment="Right" Style="{StaticResource ButtonStyle}" Width="24" Height="24" 
       6:                         Click="OnClick"></Button>
       7:             </layout:LayoutGroup>
       9:             <TextBox x:Name="linkBox" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Text="http://"></TextBox>
      10: </layout:LayoutControl>
    
    
     

    Bonus

    After you deploy the Shrinker to your corp net. (Assuming you added all the error handling and switched to a SQL Server) you can have it always available for you in the System Tray by running the Shrinker app outside of the browser.

    Download the http://community.devexpress.com/blogs/theonewith/fiji.zip and run it with the URL of your .xap file as a command line parameter.

    For example: "fiji.exe http://localhost:8081/Silverlight/Shrinker.xap"

    Download Source

    Cheers,

    Azret

     
  • Silverlight on a Desktop

         

    Silverlight or WPF/E (WPF/Everywhere) was built to be a browser plugin. Small, secure and rich*. Why? I mean why stop at the browser? Why can't we deploy it in our own sandbox? Just imagine a brand new class of applications (or applets as I like to call them).

    At this year's PDC, rumors were flying around about *good* Silverlight Hosting API possibilies in the next version. But even then, how would they work? Would they be cross platform?

    One solution to enable this new class of applications is to host everything inside Trident (IE WebBrowser). This works if you want to run your app.on Windows only. To me, this particular class of apps would need to be cross platform to succeed. Even if you disagree and say you are fine with Windows only, you'll still be limited to the Trident's sandbox. Accessing the local system would be possible via window.extern but you'll need to solve the "same origin" (same domain) security restrictions if your xap is gonna be run from say (res:// or file://). And then on top of that, what are you gonna use to host the WebBrowser (Trident), WinForms? If so go straight up WinForms or WPF.

    What else can we do? NPAPI to the rescue. Why not pretend to be a browser and load the Silverlight plugin just like a browser would. Gecko and WebKit based browsers do just that. They use the public Netscape Plugin API to load the plugins that the page needs, based on the MIME type specified in the embed. In our case it's the application/x-silverlight type. By going this route, we can control the sandbox, we can feed the functions to our applets to enable access to local system resources. We can control the same domain security restrictions. And finally, we can be cross platform and we don't need the hosting API even if it existed.

    How cool would it be if I could include a .target file in my Silverlight project, and that .target would include all the steps needed to pack my xap into a single executable?

    Welcome to project Fiji. An experimental work to do just that.

    You can download the preview bits (fiji.exe) here http://community.devexpress.com/blogs/theonewith/fiji.zip.

    To run it, simply pass in the name of your xap file in the command line: "fiji.exe C:\MySilverlightApplication.xap". (full path)

    Would love to hear what you think.

    Cheers

    Azret

  • Silverlight Line of Business Applications: Part 1 - Getting Started

         

    By now, those of you who know me personally or through the forums and blogs know that I love Silverlight. It had me at hello. A rich client side user experience, cross platform*, tiny runtime. What's not to love? In this set of posts, I want to show you some of the things that you would need, to develop a client side applet for your next big product.

    For this, we are going to develop and deploy a bug tracking app. "Bugger" and we'll use XPO for Silverlight to connect to our data. If you have not read the Silverlight and Data Access using XPO I suggest you do so as it covers the basics. The article is written for DXperience v8.2, and in v8.3, a few things have been changed. For now, the only thing we need to worry about is the name change of the data acess assemblies.

    ·    DevExpress.Data.v8.2.SL was renamed to DevExpress.AgData.v8.3

    ·    DevExpress.Xpo.v8.2.SL was renamed to  DevExpress.AgXpo.v8.3 

    Getting Started

    We'll start by creating a new ASP.NET project and add a Silverlight-enabled WCF Service to it. The web service will expose the XPO DataStore that our client will use.

          [ServiceContract(Namespace = ""), AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]

          [XmlSerializerFormat]

          public class Gateway {

                static DevExpress.Xpo.DB.IDataStore s_dataStore;

     

                static Gateway() {

                      try {                  

                            string connectionString = DevExpress.Xpo.DB.AccessConnectionProviderMultiUserThreadSafe.GetConnectionString(

                                  "C:\\Bugger.mdb", "Admin", string.Empty);

                            s_dataStore = DevExpress.Xpo.XpoDefault.GetConnectionProvider(

                                  connectionString,

                                  DevExpress.Xpo.DB.AutoCreateOption.DatabaseAndSchema);

                      }

                      catch (Exception e) {

                            System.Diagnostics.Debug.WriteLine(e.Message);

                      }

                }

     

                [OperationContract]

                public DevExpress.Xpo.DB.AutoCreateOption GetAutoCreateOption() {

                      return s_dataStore.AutoCreateOption;

                }

     

                [OperationContract]

                public DevExpress.Xpo.DB.SelectedData SelectData(DevExpress.Xpo.DB.SelectStatement[] selects) {

                      if (selects != null && selects.Length > 0 && selects[0].TableName == "XPObjectType") {

                            return new DevExpress.Xpo.DB.SelectedData(new DevExpress.Xpo.DB.SelectStatementResult());

                      }

                      return s_dataStore.SelectData(selects);

                }

     

                [OperationContract]

                public DevExpress.Xpo.DB.UpdateSchemaResult UpdateSchema(bool dontCreateIfFirstTableNotExist,

                      DevExpress.Xpo.DB.DBTable[] tables) {

                      try {

                            return s_dataStore.UpdateSchema(dontCreateIfFirstTableNotExist, tables);

                      }

                      catch (Exception e) {

                            System.Diagnostics.Debug.WriteLine(e.Message);

                            return DevExpress.Xpo.DB.UpdateSchemaResult.FirstTableNotExists;

                      }

                }

     

                [OperationContract]

                public DevExpress.Xpo.DB.ModificationResult ModifyData(DevExpress.Xpo.DB.ModificationStatement[] statements) {

                      try {

                            return s_dataStore.ModifyData(statements);

                      }

                      catch (Exception e) {

                            System.Diagnostics.Debug.WriteLine(e.Message);

                            return null;

                      }

                }

          }

    We will need to add references to DevExpress.Data.v8.3 and DevExpress.Xpo.v8.3 assemblies.

    Client Side

    On the client side, we will need to add a service reference to this Gateway web service. Instead of using the wizard however, we'll do this manually by adding a generic client proxy implementation (Gateway.cs) to our Silverlight project, downloadable here http://tv.devexpress.com/content/XPO/Gateway/Gateway.zip. We'll update the ServiceReferences.ClientConfig so that our IGateway contract is registered properly.

     <configuration>

      <system.serviceModel>

        <bindings>

          <basicHttpBinding>

            <binding name="BasicHttpBinding_Gateway" maxBufferSize="65536"

              maxReceivedMessageSize="65536">

              <security mode="None" />

            </binding>

          </basicHttpBinding>

        </bindings>

        <client>

          <endpoint address="http://localhost:50344/Gateway.svc" binding="basicHttpBinding"

            bindingConfiguration="BasicHttpBinding_Gateway" contract="DevExpress.Xpo.Xtras.IGateway"

            name="BasicHttpBinding_Gateway" />

        </client>

      </system.serviceModel>

    </configuration>

     

    Now we are ready to design our data model and start running some queries. Our data model is rather simple here. We have only one table Issues that we want to query, add to and display on the screen.

          public enum Status {

                Active,

                Closed,

          }

     

          public enum Importance {

                Normal,

                High,

                Low,

          }

     

          [OptimisticLocking(false)]

          public class Issue : PersistentBase {

                public Issue(Session session)

                      : base(session) {

                }

                Guid _id;

                [Key(AutoGenerate = true)]

                public Guid ID {

                      get { return _id; }

                      set { SetPropertyValue("ID", ref _id, value); }

                }

                string _subject;

                public string Subject {

                      get { return _subject; }

                      set { SetPropertyValue("Subject", ref _subject, value); }

                }

                string _owner;

                public string Owner {

                      get { return _owner; }

                      set { SetPropertyValue("Owner", ref _owner, value); }

                }

                Status _status;

                public Status Status {

                      get { return _status; }

                      set { SetPropertyValue("Status", ref _status, value); }

                }

                Importance _importance;

                public Importance Importance {

                      get { return _importance; }

                      set { SetPropertyValue("Importance", ref _importance, value); }

                }

                [NonPersistent]

                public Visibility HighImportance {

                      get {

                            if (Importance == Importance.High) {

                                  return Visibility.Visible;

                            }

                            else {

                                  return Visibility.Collapsed;

                            }

                      }

                }

                [NonPersistent]

                public Visibility LowImportance {

                      get {

                            if (Importance == Importance.Low) {

                                  return Visibility.Visible;

                            }

                            else {

                                  return Visibility.Collapsed;

                            }

                      }

                }

          }

    Notice the additional NonPersistent helper properties. They will come in handy when displaying data inside the AgDataGrid.

    Since we dont have any data entry forms yet and we need some mock data. Let's add some fake issues. Remember any work done againt XPO must be done asynchronously.

                             DevExpress.Xpo.Xtras.Gateway _dataStore;

                 ..

                 this._dataStore = new DevExpress.Xpo.Xtras.Gateway(this.Dispatcher);         

                 .. 

                ThreadPool.QueueUserWorkItem((state) => {

                            try {

                                  using (UnitOfWork unitOfWork = new UnitOfWork(new SimpleDataLayer(this._dataStore))) {

     

                                        var issue = new Issue(unitOfWork);

                                        issue.Owner = "Joe Smith";

                                        issue.Status = Status.Active;

                                        issue.Subject = "New Issue";

     

                                        unitOfWork.Save(issue);

                                        unitOfWork.CommitChanges();

                                  };

                            }

                            catch (Exception e) {

                                  System.Diagnostics.Debug.WriteLine(e.Message);

                            }

                      });

    Displaying the Data

    Download the free AgDataGrid and AgMenu suites here http://devexpress.com/Products/NET/Controls/Silverlight/Menu/ we'll use them a lot. Add a reference to the AgDataGrid assembly to your project and add a name space alias to it inside your page.xaml. In this case AgDataGridEx is a derived class from the standard AgDataGrid and the namespace alias is our project itself. 

          public class AgDataGridEx : AgDataGrid {

                public AgDataGridEx() {

                      this.DefaultStyleKey = typeof(AgDataGridEx);

                }

          } 

     

          <bugger:AgDataGridEx Grid.Row="1" x:Name="issuesDataGrid" ColumnsAutoWidth="True" ShowGroupPanel="Visible" SelectionMode="None">

                <bugger:AgDataGridEx.Resources>

                    <DataTemplate x:Key="ImportanceCellTemplateEx">

                        <Grid HorizontalAlignment="Center" Margin="0,0,2,0">

                            <Image Source="ImportanceRed.png" Stretch="None" Visibility="{Binding CellValue}"></Image>

                        </Grid>                   

                    </DataTemplate>

                </bugger:AgDataGridEx.Resources>

                <bugger:AgDataGridEx.Columns>

                    <bugger:AgDataGridColumnEx x:Name="issuesColumnImportance" FieldName="HighImportance" FixedWidth="True" Width="22"

                            MinColumnWidth="22" MaxColumnWidth="22" AllowResizing="False" AllowGrouping="False"

                            CellDisplayTemplate="{StaticResource ImportanceCellTemplateEx}">

                        <bugger:AgDataGridColumnEx.HeaderContent>

                            <Image Source="ImportanceBlack.png" Width="3" Height="11" Stretch="Uniform"></Image>

                        </bugger:AgDataGridColumnEx.HeaderContent>

                    </bugger:AgDataGridColumnEx>

                    <bugger:AgDataGridColumnEx FieldName="Subject" Width="250" />

                    <bugger:AgDataGridColumnEx FieldName="Owner" />

                    <bugger:AgDataGridColumnEx FieldName="Status" />

                </bugger:AgDataGridEx.Columns>

          </bugger:AgDataGridEx>

     

    Notice how the HighImportance property is used in our DataTemplate. We'll cover more of this in the next parts where we'll explore the AgDataGrid customization.

    To fetch the data from the server, simply run a query against the issues table.

          private void AsyncGetData() {

                      ThreadPool.QueueUserWorkItem(delegate(object state) {

                            UnitOfWork unitOfWork = new UnitOfWork(new SimpleDataLayer(_dataStore));

     

                            XPQuery<Issue> issues = new XPQuery<Issue>(unitOfWork);

                            var query = from t in issues

                                              select t;

     

                            var list = query.ToList<Issue>();

     

                            Dispatcher.BeginInvoke(() => {

                                  issuesDataGrid.DataSource = list;

                            });

     

                      });

          }

     

    And that's it. Well not yet, we still need to make our UI pretty, we still need to add a data entry form etc... Stay tuned for Parts 2 and 3.

    Eventually, we want to get to something like this:

     

    or rather this Stick out tongue (A free t-shirt to the first person who tells me what this is. The mock data records should give it away pretty quickly.)

    Cheers,

    Azret

     

More from DevExpress
Live Chat
Have a pre-sales question?
Need assistance with your evaluation?
We are here to help.
Chat is one of the many ways you can contact members of the DevExpress Team. We are available Monday-Friday between 8:30am and 5:00pm Pacific Time.
If you need additional product information, require pre-sales assistance, or want help with your order, write to us at info@devexpress.com or call us at
+1 (818) 844-3383.