Click here to Skip to main content
15,895,772 members
Articles / Desktop Programming / XAML

Extending GridView with Drag and Drop for Grouping and Variable Sized Items

,
Rate me:
Please Sign up or sign in to vote.
4.98/5 (23 votes)
9 Oct 2015CPOL11 min read 135.4K   8K   69  
This article describes the implementation of an extended GridView control that enables drag and drop with grouping and variable sized items.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
using C1.Xaml.Tile;
using System.Reflection;
using System.Collections.ObjectModel;
using GridViewSampleC1Tiles.DataModel;

// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238

namespace GridViewSampleC1Tiles
{
    /// <summary>
    /// This sample shows customized GridViewEx control bound to the grouped data source. 
    /// Drag&drop is implemented in the GridViewEx control.
    /// It also uses C1Tile and C1TileService to mimic the Windows 8 Start screen behavior with animated tiles.
    /// The page state is persisted at navigating from this page. 
    /// The SaveState method performs the simplest serialization
    /// of the whole _groups collection to the JSON string and adds it to the page state.
    /// The LoadState method performs deserialization of the previously saved groups or creates groups from scratch if page state is empty.
    /// GridViewSampleC1Tiles.App class serializes this state along with application state using GridViewSampleC1Tiles.Common.SuspensionManager
    /// class. For more details see GridViewSampleC1Tiles.App code and code comments.
    /// Note, depending on your data structure and size you can implement persistence in the other way or save your data to some other place.
    /// If you need more information about saving the app's state, start from this article: 
    /// http://msdn.microsoft.com/en-us/library/windows/apps/hh986968.aspx.
    /// </summary>
    public sealed partial class MainPage : GridViewSampleC1Tiles.Common.LayoutAwarePage
    {
        List<Group> _groups = new List<Group>();

        public MainPage()
        {
            C1TileService.UpdateInterval = TimeSpan.FromSeconds(2); // make update interval smaller as there are too many items
            C1TileService.MaxAnimationNumber = 3;   // allow up to 3 animations at the same time
            this.InitializeComponent();
        }

        /// <summary>
        /// Populates the page with content passed during navigation.  Any saved state is also
        /// provided when recreating a page from a prior session.
        /// </summary>
        /// <param name="navigationParameter">The parameter value passed to
        /// <see cref="Frame.Navigate(Type, Object)"/> when this page was initially requested.
        /// </param>
        /// <param name="pageState">A dictionary of state preserved by this page during an earlier
        /// session.  This will be null the first time a page is visited.</param>
        protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
        {
            base.LoadState(navigationParameter, pageState);
            if (pageState != null && pageState.Count > 0 && pageState.ContainsKey("Groups"))
            {
                // restore groups and items from the previously serialized state
                System.Runtime.Serialization.Json.DataContractJsonSerializer rootSer =
                new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(List<Group>));

                var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes((string)pageState["Groups"]));
                _groups = (List<Group>)rootSer.ReadObject(stream);

            }
            else
            {
                // if we get here for the first time and don't have serialized content, fill groups and items from scratch
                for (int j = 1; j <= 12; j++)
                {
                    Group group = Group.GetNewGroup();

                    for (int i = 1; i <= 7 + j % 3; i++)
                    {
                        group.Items.Add(new Item()
                        {
                            Id = i,
                            GroupId = group.Id
                        });
                    }
                    _groups.Add(group);
                }
            }

            UpdateDataContext();
        }

        /// <summary>
        /// Preserves state associated with this page in case the application is suspended or the
        /// page is discarded from the navigation cache.  Values must conform to the serialization
        /// requirements of <see cref="SuspensionManager.SessionState"/>.
        /// </summary>
        /// <param name="pageState">An empty dictionary to be populated with serializable state.</param>
        protected override void SaveState(Dictionary<String, Object> pageState)
        {
            // save groups and items to JSON string so that it's possible to restore page state later
            base.SaveState(pageState);

            System.Runtime.Serialization.Json.DataContractJsonSerializer rootSer =
                new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(List<Group>));
            var stream = new MemoryStream();
            rootSer.WriteObject(stream, _groups);
            string str = System.Text.Encoding.UTF8.GetString(stream.ToArray(), 0, (int)stream.Length);
            pageState.Add("Groups", str);
        }

        /// <summary>
        /// Invoked when this page is about to be displayed in a Frame.
        /// </summary>
        /// <param name="e">Event data that describes how this page was reached.  The Parameter
        /// property is typically used to configure the page.</param>
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            // restore page state
            var frameState = GridViewSampleC1Tiles.Common.SuspensionManager.SessionStateForFrame(this.Frame);
            if (frameState.ContainsKey("TilePageData"))
            {
                this.LoadState(e.Parameter, (Dictionary<String, Object>)frameState["TilePageData"]);
            }
            else
            {
                this.LoadState(e.Parameter, null);
            }
        }

        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            // save page state with "TilePageData" key
            var frameState = GridViewSampleC1Tiles.Common.SuspensionManager.SessionStateForFrame(this.Frame);
            var pageState = new Dictionary<String, Object>();
            this.SaveState(pageState);
            frameState["TilePageData"] = pageState;
        }

        /// <summary>
        /// Creates new CollectionViewSource and updates page DataContext.
        /// </summary>
        private void UpdateDataContext()
        {
            CollectionViewSource source = new CollectionViewSource();
            source.Source = _groups;
            source.ItemsPath = new PropertyPath("Items");
            source.IsSourceGrouped = true;
            this.DataContext = source;
        }

        // creates new group in the data source, if end-user drags item to the new group placeholder
        private void MyGridView_BeforeDrop(object sender, Controls.BeforeDropItemsEventArgs e)
        {
            if (e.RequestCreateNewGroup)
            {
                // create new group and re-assign datasource 
                Group group = Group.GetNewGroup();
                _groups.Insert(e.NewGroupIndex, group);
                UpdateDataContext();
            }
        }

        // removes empty groups (except the last one)
        private void MyGridView_Drop(object sender, DragEventArgs e)
        {
            bool needReset = false;
            for (int i = _groups.Count - 1; i >= 0; i--)
            {
                if (_groups[i].Items.Count == 0 && _groups.Count > 1)
                {
                    _groups.RemoveAt(i);
                    needReset = true;
                }
            }
            if (needReset)
            {
                UpdateDataContext();
            }
        }
    }

    /// <summary>
    /// This class uses some additional code for correct working with C1 tiles in the GridView.ItemTemplate.
    /// It also sets VariableSizedWrapGrid.ColumnSpanProperty for GridViewItem controls, so that every item can have different size in the VariableSizedWrapGrid.
    /// </summary>
    public class TileGridView : GridViewSampleC1Tiles.Controls.GridViewEx
    {
        protected override DependencyObject GetContainerForItemOverride()
        {
            // Use MyGridViewItem to avoid issues when we need both C1TileService.PointerDownAnimation and GridView dragging.
            return new TileGridViewItem();
        }

        // set ColumnSpan according to the business logic (maybe some GridViewSamples.Samples.Item or group properties)
        protected override void PrepareContainerForItemOverride(Windows.UI.Xaml.DependencyObject element, object item)
        {
            Item it = item as Item;
            int colSpan = 1;
            try
            {
                if (it != null)
                {
                    colSpan = it.GroupId % 2 + 1;
                    element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, colSpan);
                }
            }
            catch
            {
                element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, 1);
            }
            finally
            {
                base.PrepareContainerForItemOverride(element, item);
            }
            Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                // when TileGridViewItem visual tree is created, find the C1Tile control and apply additional settings
                IList<DependencyObject> list = new List<DependencyObject>();
                C1.Xaml.VTreeHelper.GetChildrenOfType(element, typeof(C1TileBase), ref list);
                foreach (C1TileBase tile in list)
                {
                    // unfreeze tile after changing tile content 
                    tile.IsFrozen = false;
                    // some tile animations might require explicit width setting, so set it according to the column span
                    tile.Width = colSpan == 1 ? 150 : 310;
                }
            });
        }
        protected override void ClearContainerForItemOverride(DependencyObject element, object item)
        {
            // to avoid memory leaks, freeze tile before removing
            IList<DependencyObject> list = new List<DependencyObject>();
            C1.Xaml.VTreeHelper.GetChildrenOfType(element, typeof(C1TileBase), ref list);
            foreach (C1TileBase tile in list)
            {
                tile.IsFrozen = true;
            }
            base.ClearContainerForItemOverride(element, item);
        }
    }

    /// <summary>
    /// Allows to select data template depending on some custom logic.
    /// </summary>
    public class TileGridViewItemTemplateSelector : C1.Xaml.C1DataTemplateSelector
    {
        static Random randomizer = new Random();

        protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
        {
            int templateNumber = randomizer.Next(3);
            Item sampleItem = item as Item;
            if ((sampleItem.GroupId + sampleItem.Id) % 10 == 3)
            {
                templateNumber = 3;
            }
            return Resources["ItemTemplate" + templateNumber.ToString()] as DataTemplate;
        }
    }

    /// <summary>
    /// Use the MyGridViewItem class if you need both C1TileService.PointerDownAnimation and GridView dragging functionality.
    /// This class handles pointer captures so that there are no any conflicts between C1TileService and GridView actions.
    /// </summary>
    public class TileGridViewItem : GridViewItem
    {
        protected override void OnPointerCaptureLost(PointerRoutedEventArgs e)
        {
            C1TileService.SetPreservePointerCapture(this, false);
            base.OnPointerCaptureLost(e);
        }
        protected override void OnPointerReleased(PointerRoutedEventArgs e)
        {
            C1TileService.SetPreservePointerCapture(this, false);
            base.OnPointerReleased(e);
        }
        protected override void OnPointerMoved(PointerRoutedEventArgs e)
        {
            if (e.Pointer.IsInContact)
            {
                // Tell C1TileService to not release pointer capture at moving mouse, as it might be required for dragging.
                // GridView will release pointer capture when it's done, or it will be done at PointerReleased or PointerCaptureLost.
                C1TileService.SetPreservePointerCapture(this, true);
                C1TileService.HandlePointerMoved(this, e);
            }
            base.OnPointerMoved(e);
        }
    }

}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Program Manager GrapeCity
Russian Federation Russian Federation
I'm the GrapeCity program manager for ComponentOne Studio.
If you need more info, learn about our products here: http://www.grapecity.com/

Written By
Product Manager GrapeCity
United States United States
I am the ComponentOne product manager at GrapeCity. I love .NET but especially the XAML platforms. You'll find me blogging about these awesome technologies and at various code camps, techfests and tradeshows.

Comments and Discussions