Somebody asked how you use the DataGrid in Silverlight 2 in order to load data from a web service, modify it and submit it back so I thought I'd spend 5 minutes experimenting with that.
I wrote a very simple web service with WCF;
[DataContract]
public class Person
{
[DataMember]
public int Id { get; set; }
[DataMember]
public string FirstName { get; set; }
[DataMember]
public string LastName { get; set; }
[DataMember]
public int Age { get; set; }
}
[ServiceContract]
public interface IPeopleService
{
[OperationContract]
List<Person> GetPeople();
[OperationContract]
void UpdatePeople(List<Person> inserts, List<Person> updates,
List<Person> deletes);
}
and implemented it in the simplest possible way ( no thread-safety, no concurrency );
public class PeopleService : IPeopleService
{
static PeopleService()
{
people = new Dictionary<int, Person>();
people.Add(1, new Person() { Id = 1, FirstName = "Mike", LastName = "Taulty", Age = 18 });
people.Add(2, new Person() { Id = 2, FirstName = "Mike", LastName = "Ormond", Age = 78 });
people.Add(3, new Person() { Id = 3, FirstName = "Daniel", LastName = "Moth", Age = 101});
}
public List<Person> GetPeople()
{
return (people.Values.ToList());
}
public void UpdatePeople(List<Person> inserts, List<Person> updates, List<Person> deletes)
{
if (inserts != null)
{
foreach (Person p in inserts)
{
p.Id = nextId++;
people.Add(p.Id, p);
}
}
if (deletes != null)
{
foreach (Person p in deletes)
{
people.Remove(p.Id);
}
}
if (updates != null)
{
foreach (Person p in updates)
{
Person updateEntry = people[p.Id];
updateEntry.FirstName = p.FirstName;
updateEntry.LastName = p.LastName;
updateEntry.Age = p.Age;
}
}
}
private static Dictionary<int, Person> people;
private static int nextId = 4;
}
and I made sure that the web.config file that had been edited by the tool when I inserted the WCF service had basicHttpBinding rather than the wsHttpBinding you get by default.
From there, I added in a Silverlight project and created a simple UI.
It's just a DataGrid and a couple of buttons. I added a service reference to my WCF service in order to get a proxy class to use to call the service asynchronously.
In order to populate the DataGrid, I make a call to my webservice, get the data, convert the results into an ObservableCollection<T> where T is a local class that I called DataRow;
public enum RowState
{
Clean,
Modified,
Inserted
}
public class DataRow
{
public DataRow()
{
State = RowState.Clean;
}
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public RowState State { get; set; }
}
and then I just set up the UI so that it data binds 4 text box columns to the properties on my DataRow class.
I use that RowState enum to determine for each row whether it has been modified or inserted and I also keep a separate List<DataRow> for any rows that have been deleted.
From a UI perspective, I couldn't find a simple property which I could set to get the DataGrid to display a "blank insert row" so I just added a keyboard handler for the Insert key and the Delete key and that's how I handle insert/delete from the UI. For insert, I just add a blank DataRow instance into the ObservableCollection.
At the point where the "Submit Changes..." button is pressed I just gather up which entries have been inserted, updated, deleted as arrays and send them back over to the web service.
So...all in all, there's no rocket-science here at all. The XAML for my UI ends up looking like this;
<UserControl x:Class="SilverlightApplication14.Page"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition
Height="48" />
<RowDefinition
Height="48" />
</Grid.RowDefinitions>
<Grid
x:Name="gridLoading"
Opacity="0.5"
Visibility="Collapsed">
<Rectangle
Fill="Silver"
RadiusX="10"
RadiusY="10"/>
<TextBlock
FontSize="24"
Text="Loading Grid..."
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
<d:DataGrid
x:Name="theGrid"
Margin="10">
<d:DataGrid.Columns>
<d:DataGridTextBoxColumn
Header="Id"
DisplayMemberBinding="{Binding Id}"
IsReadOnly="True"/>
<d:DataGridTextBoxColumn
Header="First Name"
DisplayMemberBinding="{Binding FirstName}" />
<d:DataGridTextBoxColumn
Header="Last Name"
DisplayMemberBinding="{Binding LastName}" />
<d:DataGridTextBoxColumn
Header="Age"
DisplayMemberBinding="{Binding Age}" />
</d:DataGrid.Columns>
</d:DataGrid>
<Button
Click="OnLoad"
Grid.Row="1"
Content="Load Data from Service"
Margin="10" />
<Button
Click="OnSave"
Grid.Row="2"
Content="Submit Changes to Service"
Margin="10"/>
</Grid>
</UserControl>
and the code behind the UI ends up looking like this ( pretty rough and ready stuff this );
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
this.Loaded += OnLoaded;
theGrid.CommittingCellEdit += OnCommittingCellEdit;
theGrid.KeyDown += OnGridKeyDown;
deletions = new List<DataRow>();
}
void OnCommittingCellEdit(object sender, DataGridCellCancelEventArgs e)
{
DataRow dr = e.Element.DataContext as DataRow;
if ((dr != null) && (dr.State != RowState.Inserted))
{
dr.State = RowState.Modified;
}
}
void OnGridKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Delete)
{
DataRow dr = theGrid.SelectedItem as DataRow;
if (dr != null)
{
data.Remove(dr);
if (dr.State != RowState.Inserted)
{
deletions.Add(dr);
}
}
}
else if (e.Key == Key.Insert)
{
data.Add(new DataRow()
{
State = RowState.Inserted
});
}
}
void OnLoaded(object sender, RoutedEventArgs e)
{
data = new ObservableCollection<DataRow>();
theGrid.DataContext = data;
theGrid.ItemsSource = data;
}
void OnSave(object sender, EventArgs args)
{
var deleted =
deletions.ConvertAll<Person>(DataRowToPerson).ToArray();
var updated = (from u in data
where u.State == RowState.Modified
select DataRowToPerson(u)).ToArray();
var inserted = (from i in data
where i.State == RowState.Inserted
select DataRowToPerson(i)).ToArray();
PeopleServiceClient proxy = new PeopleServiceClient();
proxy.UpdatePeopleCompleted += OnUpdatesCompleted;
proxy.UpdatePeopleAsync(inserted, updated, deleted);
}
void OnUpdatesCompleted(object sender, AsyncCompletedEventArgs e)
{
OnLoad(null, null);
}
static Person DataRowToPerson(DataRow row)
{
return (new Person()
{
Id = row.Id,
FirstName = row.FirstName,
LastName = row.LastName,
Age = row.Age
});
}
static DataRow PersonToDataRow(Person person)
{
return (new DataRow()
{
Id = person.Id,
FirstName = person.FirstName,
LastName = person.LastName,
Age = person.Age
});
}
void OnLoad(object sender, EventArgs args)
{
data.Clear();
deletions.Clear();
gridLoading.Visibility = Visibility.Visible;
PeopleServiceClient proxy = new PeopleServiceClient();
proxy.GetPeopleCompleted += OnGetPeopleCompleted;
proxy.GetPeopleAsync();
}
void OnGetPeopleCompleted(object sender, GetPeopleCompletedEventArgs e)
{
gridLoading.Visibility = Visibility.Collapsed;
foreach (var person in e.Result)
{
data.Add(PersonToDataRow(person));
}
}
private ObservableCollection<DataRow> data;
private List<DataRow> deletions;
}
Note that at some point I expect that you'll have to use Dispatcher.Invoke to get back to the UI thread when these asynchronous calls complete but in the current beta it seems that you don't have to do that. Maybe it'd be better practise to put it in anyway to future proof the code a bit but, as I say, that code is pretty hacky anyway :-)
I've uploaded the whole project here in case it might help anybody.
Posted
Thu, Mar 27 2008 4:09 PM
by
mtaulty