WCF RIA Services and ComplexObjects

The release of the WCF RIA Services V1.0 SP1 Beta, added support for complex type members in entities and DomainService operations. Here is a quick walkthrough.

I started with Northwind database and customers table. I refactored some of the columns (Address,City,Region, PostalCode, Country) into a complex object called “AddressDetails” and updated the mappings like we see in the screenshot. Added a DomainService based on the model.

I added a Dataform in the Silverlight application like below

<toolkit:DataForm x:Name=”dataform1″ AutoEdit=”True” AutoCommit=”True”  
                          CommandButtonsVisibility=”All”  >
            <StackPanel>
                <toolkit:DataField Label=”CustomerId”>
                    <TextBox Text=”{Binding CustomerID, Mode=TwoWay}”/>
                </toolkit:DataField>
                <toolkit:DataField Label=”ContactName”>
                    <TextBox Text=”{Binding ContactName, Mode=TwoWay}”/>
                </toolkit:DataField>
                <toolkit:DataField Label=”ContactTitle”>
                    <TextBox Text=”{Binding ContactTitle, Mode=TwoWay}”/>
                </toolkit:DataField>
                <toolkit:DataField Label=”CompanyName”>
                    <TextBox Text=”{Binding CompanyName, Mode=TwoWay}”/>
                </toolkit:DataField>
                <toolkit:DataField Label=”Address”>
                    <TextBox Text=”{Binding AddressDetails.Address, Mode=TwoWay}”/>
                </toolkit:DataField>
                <toolkit:DataField Label=”City”>
                    <TextBox Text=”{Binding AddressDetails.City, Mode=TwoWay}”/>
                </toolkit:DataField>
                <toolkit:DataField Label=”PostalCode”>
                    <TextBox Text=”{Binding AddressDetails.PostalCode, Mode=TwoWay}”/>
                </toolkit:DataField>
                <toolkit:DataField Label=”Country”>
                    <TextBox Text=”{Binding AddressDetails.Country, Mode=TwoWay}”/>
                </toolkit:DataField>
            </StackPanel>   
        </toolkit:DataForm>
Here is the code behind.

public partial class MainPage : UserControl
    {
        DomainService1 ctx = new DomainService1();

        public MainPage()
        {
            InitializeComponent();

            this.DataContext = this;
            this.Loaded += new RoutedEventHandler(MainPage_Loaded);
            dataform1.EditEnded += new EventHandler<DataFormEditEndedEventArgs>(dataform1_EditEnded);
          
        }

        void dataform1_EditEnded(object sender, DataFormEditEndedEventArgs e)
        {           
            ctx.SubmitChanges();
        }
        void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            LoadOperation<Customer> lo = ctx.Load<Customer>(ctx.GetCustomersQuery());
            lo.Completed += new EventHandler(lo_Completed);
        }

        void lo_Completed(object sender, EventArgs e)
        {
            dataform1.ItemsSource = (sender as LoadOperation<Customer>).Entities.ToList();
        }

    }

we can change data and click ok for the data to be saved. The Ok Button is not enabled, if the changes are made to the fields that are part of the complex object(“AddressDetails”). I tried to add a new customer. After filling in all the fields clicked “ok” button and got the error “AddressDetails is required”.  I added the following code to create a new instance of AddressDetails when a Customer is created
 public partial class Customer
    {
        partial void OnCreated()
        {
            this.AddressDetails = new AddressDetails();
          
        }             
    }

I did not get any error, but the new customer is not saved. The Context did not see any changes. so I had to explicitly add code to check the entitystate and add the object to the context for the new customer to be saved. I am not sure if I am missing something here.

 void dataform1_EditEnded(object sender, DataFormEditEndedEventArgs e)
        {
            if ((dataform1.CurrentItem as Customer).EntityState == EntityState.Detached)
                ctx.Customers.Add(dataform1.CurrentItem as Customer);
            ctx.SubmitChanges();
        }
We can also get the complex object by itself. I added the following code to get the AddressDetails for a given customer in the domainservice

  public AddressDetails GetCustomerAddressDetails(string customerId)
        {
            return this.ObjectContext.Customers.Where(x=>x.CustomerID == customerId).FirstOrDefault().AddressDetails;
        }

and call it like so from the client

ctx.GetCustomerAddressDetails(“ALFKI”, (io) => { AddressDetails details = io.Value; }, null);

The complexobject doesnt need to be based on an entity in the model, I added a customclass like below

    [Serializable()]
    [DataContract]
    public partial class CustomersSummary : ComplexObject
    {
        [Key]
        [DataMember]
        public string Country { get; set; }
        [DataMember]
        public int NumberOfCustomers { get; set; }
    }
wrote a method in the domainservice like below to return a list of the customobjects

  public IQueryable<CustomersSummary> GetCustomerSummary()
        {
            List<CustomersSummary> returnList=  this.ObjectContext.Customers.Where(c=>c.AddressDetails.Country !=null).GroupBy(x => x.AddressDetails.Country).Select(y => new CustomersSummary { Country = y.Key, NumberOfCustomers = y.Count() }).ToList();
            return returnList.AsQueryable();
        }

On the client the following code would get the list of customobjects.

ctx.Load<CustomersSummary>(ctx.GetCustomerSummaryQuery(), (lo) => { List<CustomersSummary> data = lo.Entities.ToList(); }, false);

Highlighting entire column in Datagrid

I came across this post on stackoverflow on highlighting an entire column when that column is sorted (clicked on header). The question was already answered. Here is another version. ItemsSource property need not be a PagedCollectionView in this version. The code need to be changed if there are any TemplateColumns defined.

using this behavior

 <sdk:DataGrid  x:Name=”datagrid1″  >
            <i:Interaction.Behaviors>
                <local:HiliteColumnBehavior/>
            </i:Interaction.Behaviors>
        </sdk:DataGrid>

Here is the behavior itself

 public class HiliteColumnBehavior : Behavior<DataGrid>
    {       
        bool isDragging = false;
        bool isDone = false;
        readonly SolidColorBrush hiliteColor = new SolidColorBrush(Colors.LightGray);
        readonly SolidColorBrush normalColor = new SolidColorBrush(Colors.White);
        DataGridColumnHeader currentSortColumnHeader;
        ScrollBar scrollbar;
        protected override void OnAttached()
        {
            base.OnAttached();
           
            AssociatedObject.ColumnHeaderDragStarted += new EventHandler<DragStartedEventArgs>(AssociatedObject_ColumnHeaderDragStarted);
            AssociatedObject.ColumnHeaderDragCompleted += new EventHandler<DragCompletedEventArgs>(AssociatedObject_ColumnHeaderDragCompleted);
            AssociatedObject.LayoutUpdated += new EventHandler(AssociatedObject_LayoutUpdated);
            AssociatedObject.SizeChanged += (s, e) => { Hilite(); };
        }

        DataGridColumnHeadersPresenter dchp;
        void AssociatedObject_LayoutUpdated(object sender, EventArgs e)
        {
            dchp = AssociatedObject.GetChildrenByType<DataGridColumnHeadersPresenter>().FirstOrDefault();
            if (dchp != null && !isDone) {
                foreach (DataGridColumnHeader dgch in dchp.Children)
                    dgch.AddHandler(DataGridColumnHeader.MouseLeftButtonUpEvent, new MouseButtonEventHandler(header_MouseLeftButtonUp), true);

                isDone = true;
            }
            if (scrollbar == null)
            {
                scrollbar = AssociatedObject.GetChildrenByType<ScrollBar>().FirstOrDefault();
                scrollbar.Scroll += new ScrollEventHandler(scrollbar_Scroll);
            }
               
        }

        void scrollbar_Scroll(object sender, ScrollEventArgs e)
        {
            if (e.ScrollEventType == ScrollEventType.EndScroll)
                Hilite();
        }

        void AssociatedObject_ColumnHeaderDragCompleted(object sender, DragCompletedEventArgs e)
        {
            isDragging = false;
        }

        void AssociatedObject_ColumnHeaderDragStarted(object sender, DragStartedEventArgs e)
        {
            isDragging = true;
        }

        void header_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            currentSortColumnHeader = sender as DataGridColumnHeader;
           
            if (!isDragging)
            {
                Hilite();
            }
           
        }

        private void Hilite()
        {
            if (currentSortColumnHeader == null)
                return;
            int columnIndex = dchp.Children.IndexOf(currentSortColumnHeader);
            List<DataGridRow> rows = AssociatedObject.GetChildrenByType<DataGridRow>();
            rows.ForEach(x => { x.GetChildrenByType<DataGridCell>().Where((y, i) => i == columnIndex).ToList().ForEach(z => z.Background = hiliteColor); });
            rows.ForEach(x => { x.GetChildrenByType<DataGridCell>().Where((y, i) => i != columnIndex).ToList().ForEach(z => z.Background = normalColor); });
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            foreach (DataGridColumnHeader dgch in dchp.Children)
                dgch.RemoveHandler(DataGridColumnHeader.MouseLeftButtonUpEvent, new MouseButtonEventHandler(header_MouseLeftButtonUp));

            if (scrollbar != null)
            {            
                scrollbar.Scroll -= new ScrollEventHandler(scrollbar_Scroll);
            }
        }
    }

    public static class Extensions
    {
        public static T FindParentOfType<T>(this FrameworkElement element)
        {
            if (element == null)
                return default(T);

            var parent = VisualTreeHelper.GetParent(element) as FrameworkElement;

            while (parent != null)
            {
                if (parent is T)
                    return (T)(object)parent;

                parent = VisualTreeHelper.GetParent(parent) as FrameworkElement;
            }
            return default(T);
        }

        // Methods
        public static List<T> GetChildrenByType<T>(this UIElement element) where T : UIElement
        {
            return element.GetChildrenByType<T>(null);
        }

        public static List<T> GetChildrenByType<T>(this UIElement element, Func<T, bool> condition) where T : UIElement
        {
            List<T> results = new List<T>();
            GetChildrenByType<T>(element, condition, results);
            return results;
        }

        private static void GetChildrenByType<T>(UIElement element, Func<T, bool> condition, List<T> results) where T : UIElement
        {
            if (element == null)
                return;
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
            {
                UIElement child = VisualTreeHelper.GetChild(element, i) as UIElement;
                if (child != null)
                {
                    T t = child as T;
                    if (t != null)
                    {
                        if (condition == null)
                        {
                            results.Add(t);
                        }
                        else if (condition(t))
                        {
                            results.Add(t);
                        }
                    }
                    GetChildrenByType<T>(child, condition, results);
                }
            }
        }

        public static bool HasChildrenByType<T>(this UIElement element, Func<T, bool> condition) where T : UIElement
        {
            return (element.GetChildrenByType<T>(condition).Count != 0);
        }
    }