Tuesday, November 15, 2016

Using Pixel shader to highlight data grid row

/// <summary>
    /// Dev Express GridControl does not support multi selection when Master-Detail rows are involved. This behavior highlights multiple rows
    /// and persist onto ViewModel. So it has the effect of multiple selection.
    /// </summary>
    public class MultiRowHighlightBehavior : Behavior<GridControl>
    {

        #region properties

        public int MaxNumberOfRowsHighlighted
        {
            get { return (int)this.GetValue(MaxNumberOfRowsHighlightedProperty); }
            set { this.SetValue(MaxNumberOfRowsHighlightedProperty, value); }
        }
        public static readonly DependencyProperty MaxNumberOfRowsHighlightedProperty = DependencyProperty.Register(
          "MaxNumberOfRowsHighlighted", typeof(int), typeof(MultiRowHighlightBehavior), new PropertyMetadata(int.MaxValue));

        // use IList, INotifyCollectionChanged to avoid generic in DP def e.g. ObservableCollection<T>

        // Pass highlighted Data Rows to VM
        public IList ViewModelHighlightedRows
        {
            get { return (IList)this.GetValue(ViewModelHighlightedRowsProperty); }
            set { this.SetValue(ViewModelHighlightedRowsProperty, value); }
        }
        public static readonly DependencyProperty ViewModelHighlightedRowsProperty = DependencyProperty.Register(
          "ViewModelHighlightedRows", typeof(IList), typeof(MultiRowHighlightBehavior), new PropertyMetadata(null));

        // need to pass in Data Rows for the GridControl, so any removal triggers highlighting updates due to row position changes.
        public INotifyCollectionChanged ViewModelDataRows
        {
            get { return (INotifyCollectionChanged)this.GetValue(ViewModelDataRowsProperty); }
            set { this.SetValue(ViewModelDataRowsProperty, value); }
        }
        public static readonly DependencyProperty ViewModelDataRowsProperty = DependencyProperty.Register(
          "ViewModelDataRows", typeof(INotifyCollectionChanged), typeof(MultiRowHighlightBehavior), new PropertyMetadata(null));


        public string HighLightingColorHex
        {
            get { return (string)this.GetValue(HighLightingColorHexProperty); }
            set { this.SetValue(HighLightingColorHexProperty, value); }
        }
        public static readonly DependencyProperty HighLightingColorHexProperty = DependencyProperty.Register(
          "HighLightingColorHex", typeof(string), typeof(MultiRowHighlightBehavior), new PropertyMetadata(DefaultColorHex));


        public TableViewHitTest HighlightingClickArea
        {
            get { return (TableViewHitTest)this.GetValue(HighlightingClickAreaProperty); }
            set { this.SetValue(HighlightingClickAreaProperty, value); }
        }
        public static readonly DependencyProperty HighlightingClickAreaProperty = DependencyProperty.Register(
          "HighlightingClickArea", typeof(TableViewHitTest), typeof(MultiRowHighlightBehavior), new PropertyMetadata(TableViewHitTest.RowIndicator));


        public bool AllowHighLightingMasterRow
        {
            get { return (bool)this.GetValue(AllowHighLightingMasterRowProperty); }
            set { this.SetValue(AllowHighLightingMasterRowProperty, value); }
        }
        public static readonly DependencyProperty AllowHighLightingMasterRowProperty = DependencyProperty.Register(
          "AllowHighLightingMasterRow", typeof(bool), typeof(MultiRowHighlightBehavior), new PropertyMetadata(false));


        public bool UseShaderEffect
        {
            get { return (bool)this.GetValue(UseShaderEffectProperty); }
            set { this.SetValue(UseShaderEffectProperty, value); }
        }
        public static readonly DependencyProperty UseShaderEffectProperty = DependencyProperty.Register(
          "UseShaderEffect", typeof(bool), typeof(MultiRowHighlightBehavior), new PropertyMetadata(true));

        private const string DefaultColorHex = "#FF3D3D3D";
        private SolidColorBrush _highLightingBrush;


        private enum HighlightingState
        {
            On,
            Off
        }

        #endregion

        #region event

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

            AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
            AssociatedObject.Loaded += AssociatedObject_Loaded;




            TrySetHighlightingBrush();
        }

        void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
        {
            var sv = VisualTreeHelpers.FindChild<ScrollViewer>(AssociatedObject);
            if (sv != null)
            {
                sv.ScrollChanged += (s, _) => { if (ViewModelHighlightedRows.Count > 0) RedoHilighting(); };
            }
        }

        protected override void OnChanged()
        {
            base.OnChanged();
            if (ViewModelDataRows != null)
            {
                ViewModelDataRows.CollectionChanged += ViewModelDataRows_CollectionChanged;
            }
            if (ViewModelHighlightedRows is INotifyCollectionChanged)  // e.g ObservableCollection<T> is IList and INotifyCollectionChanged
            {
                (ViewModelHighlightedRows as INotifyCollectionChanged).CollectionChanged += ViewModelHighlightedRows_CollectionChanged;
            }
        }

        void ViewModelHighlightedRows_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (ViewModelHighlightedRows.Count == 0) RedoHilighting(); // clear all highlights if collection cleared by outside caller.
        }

        // Rows removed by VM will be removed from highlighted rows if present
        void ViewModelDataRows_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach (var i in e.OldItems)
                {
                    if (ViewModelHighlightedRows != null) ViewModelHighlightedRows.Remove(i);
                }
                RedoHilighting();
            }
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                RedoHilighting();
            }
        }

        void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (AssociatedObject.View == null) return;
            var view = AssociatedObject.View as TableView;
            if (view == null) return;

            TableViewHitInfo hitInfo = view.CalcHitInfo(e.OriginalSource as DependencyObject);

            if (hitInfo == null) return;

            // Highlighting happens if user click on Row Header. So highlighting will not interfare with Row/Cell/Expander selection/expanding functionality
            if (hitInfo.HitTest == HighlightingClickArea)
            {
                var row = view.GetRowElementByMouseEventArgs(e) as GridRow;
                if (row == null || row.RowDataContent == null) return;
                var rowData = row.RowDataContent.DataContext as RowData;
                if (rowData == null || ViewModelHighlightedRows == null) return;

                if (!AllowHighLightingMasterRow && IsMasterRow(row) && !HasMarketDepth(rowData.Row)) return;  // use visual tree to check if row =master row, no API found

                if (ViewModelHighlightedRows.Contains(rowData.Row))
                {
                    ToggleHighlighting(row, HighlightingState.Off);
                    ViewModelHighlightedRows.Remove(rowData.Row);
                }
                else
                {
                    if (ViewModelHighlightedRows.Count >= MaxNumberOfRowsHighlighted) return;
                    ToggleHighlighting(row, HighlightingState.On);
                    ViewModelHighlightedRows.Add(rowData.Row);
                }
            }
        }

        private bool HasMarketDepth(object rowDataObject)
        {
            try
            {
                PropertyInfo propInfo = rowDataObject.GetType().GetProperty("HasMarketDepth", BindingFlags.Instance | BindingFlags.Public);

                if (propInfo != null)
                {
                    var val = propInfo.GetValue(rowDataObject, null);
                    return (bool)val;
                }
            }
            catch (Exception)
            {


            }
            return false;
        }

        #endregion

        #region change View and VM, etc.

        // change view only, not ViewModel
        private void ToggleHighlighting(GridRow row, HighlightingState hState)
        {
            try
            {
                var rowIndControl = VisualTreeHelpers.FindChild<RowIndicatorControl>(row);
                var ricBorder = VisualTreeHelpers.FindChild<Border>(rowIndControl);

                SolidColorBrush toggleBrush;
                Effect toggleEff;
                double toggleThickness;
                if (hState == HighlightingState.On)
                {
                    toggleBrush = _highLightingBrush;
                    toggleThickness = 1;
                    toggleEff = new ColorToneEffect() { DarkColor = Colors.Transparent, LightColor = Colors.LightBlue, ToneAmount = 0.1 };
                }
                else
                {
                    toggleBrush = new SolidColorBrush(Colors.Transparent);
                    toggleThickness = 0;
                    toggleEff = null;
                }

                var rowContentBorder = VisualTreeHelpers.FindChild<Border>(row, "RowContentBorder");
                var fixNoneCellstBorder = VisualTreeHelpers.FindChild<Border>(row, "PART_FixedNoneCellsBorder");
                var detailButtonBorder = VisualTreeHelpers.FindChild<Border>(row, "PART_DetailButtonBorder");

                if (UseShaderEffect)  // Shader effect will highlighting on top of all colors across a row
                {
                    ricBorder.Effect = toggleEff;
                    rowContentBorder.Effect = toggleEff;
                    fixNoneCellstBorder.Effect = toggleEff;
                    detailButtonBorder.Effect = toggleEff;
                }
                else
                {
                    ricBorder.Background = toggleBrush;
                    ricBorder.BorderThickness = new Thickness(toggleThickness, toggleThickness, 0, toggleThickness);
                    ricBorder.BorderBrush = toggleBrush;

                    rowContentBorder.BorderBrush = toggleBrush;
                    rowContentBorder.BorderThickness = new Thickness(0, toggleThickness, 0, toggleThickness < 0.5 ? 0.0 : 1.0);

                    fixNoneCellstBorder.BorderBrush = toggleBrush;
                    fixNoneCellstBorder.BorderThickness = new Thickness(0, toggleThickness, 0,
                        toggleThickness < 0.5 ? 0.0 : 1.0);

                    detailButtonBorder.BorderBrush = toggleBrush;
                    detailButtonBorder.BorderThickness = new Thickness(0, toggleThickness, 0, toggleThickness);
                }

            }
            catch (Exception)
            {


            }
        }

        private static bool IsMasterRow(GridRow row)
        {
            try
            {
                var masterRowExpandBorder = VisualTreeHelpers.FindChild<Border>(row, "PART_DetailButtonBorder");
                if (masterRowExpandBorder == null) return false;
                var isMasterRow = VisualTreeHelpers.FindChild<Grid>(masterRowExpandBorder) != null;
                return isMasterRow;
            }
            catch (Exception)
            {
                return false;  // if not sure, let user highlight and un-highlight
            }

        }

        void RedoHilighting()
        {
            if (AssociatedObject == null || AssociatedObject.View == null) return;
            var view = AssociatedObject.View as TableView;
            if (view == null) return;

            var hp = VisualTreeHelpers.FindChild<HierarchyPanel>(view);
            if (hp == null) return;

            AssociatedObject.RefreshData();
            foreach (var c in hp.Children)
            {
                var row = c as GridRow;
                if (row == null || row.RowDataContent == null) continue;
                var rowData = row.RowDataContent.DataContext as RowData;
                if (rowData == null) continue;

                if (ViewModelHighlightedRows.Contains(rowData.Row))
                {
                    ToggleHighlighting(row, HighlightingState.On);
                }
                else
                {
                    ToggleHighlighting(row, HighlightingState.Off);
                }
            }
        }

        private void TrySetHighlightingBrush()
        {
            try
            {
                _highLightingBrush = (new BrushConverter().ConvertFrom(HighLightingColorHex)) as SolidColorBrush;
            }
            catch (Exception)
            {
                _highLightingBrush = (new BrushConverter().ConvertFrom(DefaultColorHex)) as SolidColorBrush;

            }

            if (_highLightingBrush != null) _highLightingBrush.Freeze();
        }

        #endregion

    }

No comments:

Post a Comment