Data Source Controls, Part 5: Design Time Functionality

 

Nikhil Kothari
Microsoft Corporation

November 2005

Applies to:
   Microsoft Visual Studio 2005
   Microsoft ASP.NET 2.0
   Data Source Controls

Summary: In part 5, the final installment of this series, I will cover adding design-time functionality and enabling design-time experiences associated with a data source control, and its associated control designer. (7 printed pages)

Click here to download the code sample for this article.

Contents

Introduction
The Sample

So far, this series has been focused on runtime aspects of data source controls. In the final segment, I'll cover design-time aspects of authoring these controls. By virtue of encapsulating data access logic into a server control, we've enabled developers to create a design-time experience for data. Just as data source controls enable data-bound controls to be more intelligent at runtime, their design-time counterparts allow the corresponding data-bound control designers to be smarter on the design surface.

Introduction

The two most important pieces of information that a data source control designer can provide are design-time data, and schema information. Design-time data might be live or real, or may be faked up, but given that the data source is aware of its domain, the faked up data can sometimes be a better approximation. Design-time data is used to perform data-binding within data-bound controls on the design surface. Schema information can be used by data-bound controls to populate design-time UI, such as field pickers. These are the minimum bar for any data source designer to implement. Beyond that, a data source control designer can offer configuration UI, and use design-time services to enhance the developer experience.

The Sample

The first step is to write the basic designer. As you'll see, the DataSourceDesigner is similar to the DataSourceControl class in that it offers a list of view names, and designer versions of DataSourceView instances.

public class WeatherDataSourceDesigner : DataSourceDesigner {

    private Weather _sampleWeather;
    private bool _realData;

    private bool RealData {
        get { return _realData; }
    }

    private Weather SampleWeather {
        get {
            if (_sampleWeather == null) {
                return new Weather("98052", "Redmond, WA (98052)",
                                    "70", "Sunny", "n/a", DateTime.Today.ToString("D"),
                                    "[Sample Information]");
            }
            return _sampleWeather;
        }
    }

    public override DesignerDataSourceView GetView(string viewName) {
        if ((String.IsNullOrEmpty(viewName) ||
            (String.Compare(viewName, WeatherDataSource.CurrentConditionsViewName,
                            StringComparison.OrdinalIgnoreCase) == 0))) {
            return new WeatherDesignerDataSourceView(this, viewName);
        }

        throw new ArgumentOutOfRangeException("viewName");
    }

    public override string[] GetViewNames() {
        return new string[] { WeatherDataSource.CurrentConditionsViewName };
    }

    private sealed class WeatherDesignerDataSourceView : DesignerDataSourceView {

        private WeatherDataSourceDesigner _owner;

        public WeatherDesignerDataSourceView(WeatherDataSourceDesigner owner, string viewName)
            : base(owner, viewName) {
            _owner = owner;
        }

        public override IDataSourceViewSchema Schema {
            get {
                TypeSchema ts = new TypeSchema(typeof(Weather));
                return ts.GetViews()[0];
            }
        }

        public override IEnumerable GetDesignTimeData(int minimumRows, out bool isSampleData) {
            Weather sampleWeather = _owner.SampleWeather;
            Weather[] list = new Weather[minimumRows];

            for (int i = 0; i < minimumRows; i++) {
                list[i] = sampleWeather;
            }

            isSampleData = (_owner.RealData == false);
            return list;
        }
    }
}

The view implementation here provides a design-time representation of the weather data, and by doing so the display starts to make more sense in the designer than random fake data. It also offers schema information that populates field pickers such as those in the data-bindings dialog box, so the developer doesn't have to guess column or property names. I've used a helper class, TypeSchema, that reflects on a type to build schema, but you can choose to implement IDataSourceViewSchema directly if you desire to do so. The screenshot below shows this basic functionality in action:

ms364053.datasrc_fig01(en-US,VS.80).gif

Figure 1. Implementing IdataSourceViewSchema directly

The RealData property might intrigue you. Yes, the next step is to pull in real data at design-time! In order to allow the developer to enter a zip code, I'll use the smart tasks panel, which will consist of a textbox to allow zip code entry, and an "Update" link to tell the data source to fetch the weather information.

public class WeatherDataSourceDesigner : DataSourceDesigner {

    private string _zipCode;

    private WeatherService _ws;
    private IAsyncResult _ar;
    private Timer _timer;

    public override DesignerActionListCollection ActionLists {
        get {
            // Create the DesignerActionList and hand it out
        }
    }

    // Used by the DesignerActionList
    private string ZipCode {
        get { return _zipCode; }
        set { _zipCode = value; }
    }

    private void OnTimerTick(object sender, EventArgs e) {
        if (_ar != null) {
            return;
        }
        _timer.Enabled = false;
        OnDataSourceChanged(EventArgs.Empty);
    }

    private void OnWeatherAvailable(IAsyncResult result) {
        Weather sampleWeather = ((WeatherService)result.AsyncState).EndGetWeather(result);

        if (_ar == result) {
            _sampleWeather = sampleWeather;
            _realData = true;

            _ar = null;
            _ws = null;
        }
    }

    // Called by the DesignerActionList
    private void UpdateSample() {
        if (String.IsNullOrEmpty(ZipCode)) {
            _sampleWeather = null;
        }
        else {
            if (_timer == null) {
                _timer = new System.Windows.Forms.Timer();
                _timer.Interval = 1000;
                _timer.Tick += new EventHandler(this.OnTimerTick);
            }

            _ws = new WeatherService(ZipCode);
            _ar = _ws.BeginGetWeather(new AsyncCallback(OnWeatherAvailable), _ws);
            _timer.Enabled = true;
        }
    }

    private sealed class DataActionList : DesignerActionList {
        // Code for the individual action items
    }
}

The designer uses the same WeatherService as the runtime data source control does. I chose to issue asynchronous requests, so that the tool does not freeze while this is happening. When the data arrives, the new weather information is cached, and OnDataSourceChanged is called, which (again, like the runtime) tells the data-bound control designer to requery the view for design-time data, and recreate its design-time HTML and refresh its UI. It does so by requesting the design surface to update the design-time HTML. However, there is a gotcha here. The async callback occurs on a different thread than the primary UI thread in the application (Microsoft Visual Studio here). Microsoft Windows, Windows Forms, and COM (the underlying design surface is COM-based) do not typically allow multi-threaded access. Hence, you cannot directly request design-time HTML updates on the callback thread. To work around this, I use a UI Timer that is created on the main thread (very important), so its Tick event also occurs on that thread. In the tick handler, I call OnDataSourceChanged once the weather information has been retrieved in the async callback.

As I explained in part 4, it is very important to not depend on async solely to improve performance. User experience matters in the tool, as well. The next step, then, is to use the component designer state service, which is the equivalent of cache in the designer.

public class WeatherDataSourceDesigner : DataSourceDesigner {

    public override void Initialize(IComponent component) {
        base.Initialize(component);

        IComponentDesignerStateService stateService =
            (IComponentDesignerStateService)GetService(typeof(IComponentDesignerStateService));
        if (stateService != null) {
            _zipCode = (string)stateService.GetState(component, "ZipCode");

            string[] weatherInfo = (string[])stateService.GetState(component, "WeatherInfo");
            if (weatherInfo != null) {
                _sampleWeather = new Weather(_zipCode,
                                             weatherInfo[0],    // location
                                             weatherInfo[1],    // temperature
                                             weatherInfo[2],    // description
                                             weatherInfo[3],    // iconType
                                             weatherInfo[4],    // lastUpdated
                                             weatherInfo[5]);   // lastRetrieved
            }
        }
    }

    private void OnTimerTick(object sender, EventArgs e) {
        ...

        IComponentDesignerStateService stateService =
            (IComponentDesignerStateService)GetService(typeof(IComponentDesignerStateService));
        if (stateService != null) {
            stateService.SetState(Component, "ZipCode", _zipCode);

            string[] weatherInfo = new string[] {
                                       _sampleWeather.Location,
                                       _sampleWeather.Temperature,
                                       _sampleWeather.Description,
                                       _sampleWeather.IconType,
                                       _sampleWeather.LastUpdated,
                                       _sampleWeather.LastRetrieved };
            stateService.SetState(Component, "WeatherInfo", weatherInfo);
        }
    }
}

By storing data into the cache, it is available not only across view switches (source and design), but also across IDE sessions. This allows the page to load quickly while showing previously cached data... a subtle, but very useful, behavior. Data is cached per control per page per user on the local development machine.

 

About the author

Nikhil Kothari is an architect on the Web Platform and Tools team at Microsoft (which delivers IIS, ASP.NET, and Visual Studio Web development tools). Specifically, he is responsible for the overall Web Forms (a.k.a. server controls, a.k.a. page framework) feature area. He's been working on the team since the early XSP and ASP+ days; prior to his current role, he led the development of the page framework and several of the controls you can find on the ASP.NET toolbox today.

© Microsoft Corporation. All rights reserved.