/// <summary> /// Creates a new virtual cell for the report. /// </summary> /// <param name="iContent">The content of the cell.</param> /// <param name="reportColumn">The row to which the cell belongs.</param> /// <param name="reportRow">The column to which the cell belongs.</param> public ReportCell(IContent iContent, ReportColumn reportColumn, ReportRow reportRow) { // Initialize the object. this.IsObsolete = false; this.isFocused = false; this.Content = iContent; this.ReportColumn = reportColumn; this.ReportRow = reportRow; }
/// <summary> /// Creates an explicitly empty cell for the report. /// </summary> private ReportCell() { // Initialize the object. this.IsObsolete = false; this.isFocused = false; this.Content = null; this.ReportColumn = null; this.ReportRow = null; }
/// <summary> /// Recursively add a column to the collection of rows. /// </summary> /// <param name="iContent">The content of the current row in the recursion.</param> /// <param name="rowTemplate">The template describing the layout of data in the current row.</param> internal void RecursivelyAddColumns(IContent iContent, RowTemplate rowTemplate) { // This section of code will create a virtual cell where data can be found and coordinates in the document space assigned. The existing row is // pulled apart and the properties are examined. If a template has been associated with the property's type, then an association can be made // between the row and the column where this property should be displayed. ReportRow reportRow = null; if (this.dictionary.TryGetValue(iContent.Key, out reportRow)) { foreach (PropertyInfo propertyInfo in iContent.GetType().GetProperties()) { IContent childContent = propertyInfo.GetValue(iContent, null) as IContent; if (childContent != null) { string columnId; if (this.reportGrid.reportFieldCollection.ColumnIdMap.TryGetValue(childContent.GetType(), out columnId)) { ReportColumn reportColumn; if (this.reportGrid.reportColumnCollection.TryGetValue(columnId, out reportColumn)) { if (!reportRow.ContainsKey(reportColumn)) { reportRow.Add(reportColumn, new ReportCell(childContent, reportColumn, reportRow)); } } } } } } // This will recurse into the report's hierarchy. Each row template can specify a child row and the property that is // used to create that child row. These, in turn, can be recursed into ad infinitum. foreach (RowTemplate childRowDefinition in rowTemplate.Children) { PropertyInfo propertyInfo = iContent.GetType().GetProperty(childRowDefinition.Path); Object property = propertyInfo.GetGetMethod().Invoke(iContent, null); if (property is IEnumerable) { IEnumerable iEnumerable = property as IEnumerable; foreach (IContent childContent in iEnumerable) { RecursivelyAddColumns(childContent, childRowDefinition); } } } }
/// <summary> /// Removes a row from the collection. /// </summary> /// <param name="reportRow">The row to be removed.</param> public void Remove(ReportRow reportRow) { // Remove the row from this collection. this.dictionary.Remove(reportRow); // This will remove the visual elements from the screen that are associated with this row. foreach (ReportCanvas reportCanvas in this.reportGrid.reportCanvases) { reportCanvas.Remove(reportRow); } // The report needs to purge itself of obviated user interface elements from the obsolete columns. this.viewport = this.reportGrid.reportCanvases[3].Viewport; if ((viewport.Top <= reportRow.Top && reportRow.Top < viewport.Bottom) || (viewport.Top <= reportRow.Top + reportRow.Height && reportRow.Top + reportRow.Height < viewport.Bottom)) { this.reportGrid.InvalidateMeasure(); } }
/// <summary> /// Removes a ReportRow from the canvas. /// </summary> /// <param name="columnDefinition">The description of the column to be removed.</param> internal void Remove(ReportRow reportRow) { // Each visual element that matches the specified report row will be recycled and the data connection to the underlying // cell severed. for (int childIndex = 0; childIndex < this.Children.Count;) { UIElement uiElement = this.Children[childIndex]; if (uiElement is FrameworkElement) { FrameworkElement frameworkElement = uiElement as FrameworkElement; ReportCell reportCell = DynamicReport.GetCell(frameworkElement); if (reportCell != null && reportCell.ReportRow == reportRow) { this.Remove(reportCell); } else { childIndex++; } } } }
/// <summary> /// Constructs a collection of report rows based on the data content and the templates that describes the row. /// </summary> /// <param name="iContent">The content to be displayed in the row.</param> /// <param name="rowTemplate">The template that describes how the data is presented.</param> private void RecursivelyUpdateRow(IContent iContent, RowTemplate rowTemplate) { // Each IContent object has a unique key used to associate it with a report row. If a given report row has already has been mapped to the // IContent's key, then the contents of that row will be updated. Otherwise, a new row is created and associated with the IContent. ReportRow reportRow = null; if (iContent.Key != null && this.dictionary.TryGetValue(iContent.Key, out reportRow)) { // Any change to the location or the height of the row will be animated so long as the current or target location is visible. There is no // sense in animating the movement of invisible elements of the report. if (reportRow.Top != this.height || reportRow.Height != rowTemplate.Height) { // At this point it has been determined that the row has moved. An animation sequence will be created for those rows that appear in or // disappear from the viewport. Boolean isRowVisible = (this.viewport.Top <= reportRow.Top && reportRow.Top < this.viewport.Bottom) || (this.viewport.Top <= reportRow.Top + reportRow.Height && reportRow.Top + reportRow.Height < this.viewport.Bottom); Boolean willRowBeVisible = (this.viewport.Top <= this.height && this.height < this.viewport.Bottom) || (this.viewport.Top <= this.height + rowTemplate.Height && this.height + rowTemplate.Height < this.viewport.Bottom); // This creates a list of rows who's ZIndex must be set for the animation sequence. if (isRowVisible || willRowBeVisible) { // At this point the report row either is currently visible or will be visible and has been moved or resized. This means that the // visible part of the virtual document should be measured. Items that have become visible will need to be instantiated and items that // are no longer visible need to be recycled. this.isMeasurementRequired = true; // The ZIndex of this row is set such that the rows that move the greatest distance will appear to float above the ones that only move a little. This scheme // makes the movement appear more natural. reportRow.ZIndex = Math.Abs(reportRow.Ordinal - this.ordinal); // When all the report rows have been examined and the Z order of the visible rows set, this list is used to set the ZIndex of all the // graphical elements associated with this row. this.animatedRows.Add(reportRow); } // The order of the row in the report is used for setting graphical cues such as shading and Z order. reportRow.Ordinal = this.ordinal; // If the location occupied by this row has changed then an animation sequence is constructed to move a row from one place to another. If // the row isn't visible or is not going to be visible, then just the location is modified. if (reportRow.Top != this.height) { // This will move the row to its proper position in the virtual document. reportRow.Top = this.height; // The starting location is clipped by the visible part of the report. This prevents a row that is moved to a far distant place from // moving too fast to be seen on the screen. // HACK - This should be repaired when time permits. // Double startLocation = (reportRow.ActualTop < viewport.Top) ? viewport.Top - reportRow.Height : // (reportRow.ActualTop > viewport.Bottom) ? viewport.Bottom : reportRow.ActualTop; Double startLocation = reportRow.ActualTop; // The ending location is also clipped by the visible part of the report for the same reason. Without this code, rows appear to // disappear instantly from the screen instead of moving smoothly to the nearest edge. // HACK - This should be repaired when time permits. // Double endLocation = (reportRow.Top + reportRow.Height < viewport.Top) ? viewport.Top - reportRow.Height : // (reportRow.Top > viewport.Bottom) ? viewport.Bottom : reportRow.Top; Double endLocation = reportRow.Top; // This animates the movement of the row from its current position to the new position. If the row is no longer visible then any // animation that was associated with that row will be terminated. if (isRowVisible || willRowBeVisible) { DoubleAnimation topAnimation = new DoubleAnimation(); Storyline.SetTargetObject(topAnimation, reportRow); Storyline.SetTargetProperty(topAnimation, ReportRow.ActualTopProperty); topAnimation.From = startLocation; topAnimation.To = endLocation; topAnimation.Duration = this.reportGrid.Duration; this.storyline.Children.Add(topAnimation); } else { if (reportRow.HasAnimatedProperties) { reportRow.ApplyAnimationClock(ReportRow.ActualTopProperty, null); } reportRow.ActualTop = reportRow.Top; } } // If the height occupied by this row has changed then an animation sequence is constructed to resize the row. If the row isn't visible or // is not going to be visible, then the property is modified without an animation sequence. This saves the processor from doing work when // an element isn't visible. if (reportRow.Height != rowTemplate.Height) { // This sets the height of the row. reportRow.Height = rowTemplate.Height; // This will animate the change in the row's height from the current height to the new target height. if (isRowVisible || willRowBeVisible) { DoubleAnimation heightAnimation = new DoubleAnimation(); Storyline.SetTargetObject(heightAnimation, reportRow); Storyline.SetTargetProperty(heightAnimation, ReportRow.ActualHeightProperty); heightAnimation.From = reportRow.ActualHeight; heightAnimation.To = reportRow.Height; heightAnimation.Duration = this.reportGrid.Duration; this.storyline.Children.Add(heightAnimation); } else { if (reportRow.HasAnimatedProperties) { reportRow.ApplyAnimationClock(ReportRow.ActualHeightProperty, null); } reportRow.ActualHeight = reportRow.Height; } } } // A virtual method is used to copy the current content over the existing content. The event handlers will take // care of propagating the content and properties of the data to the screen elements. reportRow.IContent.Copy(iContent); // This indicates that the row is still in use and shouldn't be purged. reportRow.IsObsolete = false; } else { // This is where new rows are created to hold the given content and added to the report. reportRow = new ReportRow(); reportRow.IContent = iContent; this.dictionary.Add(iContent.Key, reportRow); // The order of the row in the report drives visual cues such as alternate line shading and Z order properties. reportRow.Ordinal = this.ordinal; // The target location and height are initialized here without animation. reportRow.Top = this.height; reportRow.Height = rowTemplate.Height; // The actual top and height used when rendering the rows. reportRow.ActualTop = reportRow.Top; reportRow.ActualHeight = reportRow.Height; // Rows that are added to the visible portion of the display will trigger the 'MeasureOverride' which will instantiate the new report elements. if ((this.viewport.Top <= reportRow.Top && reportRow.Top < this.viewport.Bottom) || (this.viewport.Top <= reportRow.Top + reportRow.Height && reportRow.Top + reportRow.Height < this.viewport.Bottom)) { this.isMeasurementRequired = true; } // The report row is really just a container for report cells. The System.Reflection library is used to rip apart the content and create // columns that are tightly bound to the type of data found in the incoming record. foreach (PropertyInfo propertyInfo in iContent.GetType().GetProperties()) { IContent childContent = propertyInfo.GetValue(iContent, null) as IContent; if (childContent != null) { string columnId; if (this.reportGrid.reportFieldCollection.ColumnIdMap.TryGetValue(childContent.GetType(), out columnId)) { ReportColumn reportColumn; if (this.reportGrid.reportColumnCollection.TryGetValue(columnId, out reportColumn)) { reportRow.Add(reportColumn, new ReportCell(childContent, reportColumn, reportRow)); } } } } } // This is used to keep track of the order of the row in the report. this.ordinal++; // This moves the virtual cursor up to the next available row in the virtual space. this.height += reportRow.Height; // The report can have any number of levels in the hierarchical arrangment or the rows. This will recurse into the template that defines the // outline of the report and generate any rows that can be matched to the templates that are children of the current template. The 'Path' property // on the template rows indicate which properties of the content row to follow when recursing. foreach (RowTemplate childRowDefinition in rowTemplate.Children) { PropertyInfo propertyInfo = iContent.GetType().GetProperty(childRowDefinition.Path); if (propertyInfo != null) { Object property = propertyInfo.GetValue(iContent, null); if (property is IEnumerable) { IEnumerable iEnumerable = property as IEnumerable; foreach (IContent childContent in iEnumerable) { RecursivelyUpdateRow(childContent, childRowDefinition); } } } } }
public RowDefinitionsEventArgs(ReportRow reportRow) { // Initialize the object. this.Rows = new List <ReportRow>(); this.Rows.Add(reportRow); }
/// <summary> /// Create the event arguments. /// </summary> /// <param name="row">The row the cell is in.</param> /// <param name="cell">The cell itself.</param> /// <param name="property">The property that changed.</param> public ReportCellPropertyChangedEventArgs(ReportRow row, ReportCell cell, String property) { this.row = row; this.cell = cell; this.property = property; }
/// <summary> /// Handles the mouse button being released over a row header. /// </summary> /// <param name="state">The thread initialization parameter.</param> protected override void OnMouseUp(MouseButtonEventArgs e) { // This gets the location of the mouse in document coordinates. Point mouseLocation = e.GetPosition(this); // Evaluate the state of the keyboard. Key combinations involving the shift and control keys will alter the areas that // are selected. bool isShiftKeyPressed = ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift); bool isControlKeyPressed = ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control); // The target row for any movement operations is the last row selected. ReportRow anchorRow = this.ReportGrid.Rows.FindRowAt(this.anchorPoint.Y); // The mouse state indications what action should be taken when the mouse button is released. switch (this.mouseState) { case MouseState.ResizingRow: // At the conclusion of the resizing operation a decision is made to remove or change the width of a row. If // the size is reduced to zero (or less), this gesture is taken as a command to remove the row. Any positive // value for a width will result in a row change command. double rowHeight = anchorRow.Height + (mouseLocation.Y - this.anchorPoint.Y); rowHeight = rowHeight < 0.0 ? 0.0 : rowHeight; if (this.ResizeMouseMove != null) { this.ResizeMouseMove(this, new ResizeRowEventArgs(anchorRow, rowHeight, true)); } // Note - this is where the logic should go for removing or resizing a row. It should probably bubble up through // the Report and be handled by some higher logic that determins if removing or resizing a row is a legal action. // Currently it is not supported. // This will hide the popup until it is needed again. this.rowHeightPopup.IsOpen = false; // This is a momentary button: the column heading will loose the selection state when the button is released. foreach (ReportCell reportCell in this.headerCells) { reportCell.IsSelected = false; } break; case MouseState.DraggingRow: // The action taken when the dragging operation is complete depends on whether a valid destination is selected or // whether the row is meant to be deleted. switch (this.destinationState) { case DropAction.Select: // This will move the row from its current location to the desired location. if (this.destinationPopup.Visibility == Visibility.Visible) { // TODO - This should generate an event to move a row from one place to another. } break; case DropAction.Delete: // This will delete the row from the view. this.ReportGrid.Rows.Remove(anchorRow); break; } // This is a momentary button: the column heading will loose the selection state when the button is released. foreach (ReportCell reportCell in this.headerCells) { reportCell.IsSelected = false; } break; } // This resets the state of the mouse for the next operation. this.mouseState = MouseState.ButtonUp; if (this.headerPopup.IsOpen) { this.headerPopup.IsOpen = false; } // Hide the destination cursor when the mouse is released. if (this.destinationPopup.IsOpen) { this.destinationPopup.IsOpen = false; } // Release the Hounds, errr... mouse. Mouse.Capture(null); }
/// <summary> /// Handles the movement of the mouse in the row header. /// </summary> /// <param name="e">The mouse movement event arguments.</param> protected override void OnMouseMove(MouseEventArgs e) { // This gets the location of the mouse in document coordinates. Point mouseLocation = e.GetPosition(this); // The anchorRow describes the starting row for an extended selection. The selectedRow is the one over which // the mouse is currently. ReportRow selectedRow = this.ReportGrid.Rows.FindRowAt(mouseLocation.Y); ReportRow anchorRow = this.ReportGrid.Rows.FindRowAt(this.anchorPoint.Y); // The action taken by a mouse movement in the row heading of a viewer is driven by a set of states. These states // are driven, in turn, by where the mouse started and what buttons are pressed. They can instruct the viewer to // resize the rows, move the row, delete the row or sort the row. switch (this.mouseState) { case MouseState.Selecting: // When dragging the mouse around the row headings, the most recent range will be replaced. The new range consist // of every row between the anchor row and the row over which the mouse is currently. // HACK - Fix this when the selected range is guaranteed to have rows in it. if (this.selectedRanges.Count > 0) { List <ReportRow> lastRange = this.selectedRanges[this.selectedRanges.Count - 1]; if ((lastRange[lastRange.Count - 1] != selectedRow) && (selectedRow != null)) { // This will select each row between the last selected row and the one just selected. Note that the // list of rows is distinct, so duplicate rows are ignored. ReportRow firstRow = anchorRow.Top < selectedRow.Top ? anchorRow : selectedRow; ReportRow secondRow = anchorRow.Top < selectedRow.Top ? selectedRow : anchorRow; lastRange.Clear(); foreach (ReportRow reportRow in this.ReportGrid.Rows) { if (firstRow.Top <= reportRow.Top && reportRow.Top <= secondRow.Top) { lastRange.Add(reportRow); } } // Add the row Header to the range also so the it will be highlight too. lastRange.Add(ReportGrid.reportRowCollection.FindRowAt(0)); // This will select all the cells in the range of rows that have been selected. Everything else will be // cleared. The active cell -- the one that has the input focus -- is the first cell of the anchor row. SelectRows(); } } break; case MouseState.ResizingRow: // This will calculate the current row width based on the difference between the anchor point and the current mouse // location. double rowHeight = selectedRow.Height + (mouseLocation.Y - this.anchorPoint.Y); // The owner of the header canvas is called to draw the row boundary line so that it follows the mouse when the // user is resizing the rows. This operation can't be done from this canvas because the size of the scrolling // canvas -- where the row boundary cursor line is displayed -- isn't known here. if (this.ResizeMouseMove != null) { this.ResizeMouseMove(this, new ResizeRowEventArgs(selectedRow, rowHeight, false)); } // As the size of the row is dragged around, the Popup widow displays a human readable form of the width. this.rowHeightPopup.Content = rowHeight; break; case MouseState.ButtonDown: // When the user presses the top mouse button, they initiate some drag operation and the mouse activity is // captured by the row header window. If the user is simply moving the mouse over the window, then feedback is // given in the shape of the cursor. This formula determins if the mouse has moved an absolute distance of four // pixels from the original location. If it has, the user has selected a movement operation for the row. // Otherwise, the mouse operation will be interpreted as a request for a new sort order when the top mouse button // is lifted. if (Math.Sqrt(Math.Pow(mouseLocation.Y - this.mouseDownLocation.Y, 2.0) + Math.Pow(mouseLocation.Y - this.mouseDownLocation.Y, 2.0)) > DynamicReport.headerDragTrigger) { // At this point the mouse movements are intepreted as drag-and-drop operations for the row headers. The // drop states determines what happens when the mouse button is released. It can either be moved, deleted or // have no action taken. this.mouseState = MouseState.DraggingRow; this.destinationState = DropAction.NoAction; // When dragging a row, the proposed destination appears as a set of two red arrows marking where the row // will reside if dropped. The scale of the destination arrows must match the scale of the report. this.destinationPopup.Scale = this.ReportGrid.DynamicReport.Scale; this.destinationPopup.Visibility = Visibility.Hidden; this.destinationPopup.IsOpen = true; // This sets up the dragging operation by creating a destination cursor (the red arrows that point to where the // row will 'snap' into place), a row cursor (it looks like the row header was ripped out of the page) // and positions the row cursor at the tip of the current mouse location. this.headerPopup.Resources = this.ReportGrid.Resources; this.headerPopup.Content = this.headerCells; this.headerPopup.Scale = this.ReportGrid.DynamicReport.Scale; this.headerPopup.Height = selectedRow.Height; this.headerPopup.Width = this.ActualWidth; this.headerPopup.Location = mouseLocation; this.headerPopup.IsOpen = true; } break; case MouseState.DraggingRow: // If the window that contains the row headings contains the cursor, then it's possible that a destination is // selected for the row drag-and-drop operation. If the cursor is outside of the header quadrant, the row // will be deleted when the mouse button is released. if (this.Viewport.Contains(mouseLocation)) { // Any operation inside the visible header gets the basic pointing arrow for a cursor. this.Cursor = this.selectRow; // When the mouse is inside the header quadrant but there is no destination selected, then nothing will happen // when the mouse button is release. this.destinationState = DropAction.NoAction; // This attempts to find a destination for the row operation. if (selectedRow != null) { // A row can't be its own destination. if (anchorRow != selectedRow) { // A destination is selected if the left edge of the target column is entirely visible in the header // quadrant and the left half of the column header contains the current mouse location. Rect testAreaTop = new Rect(this.Viewport.Left, selectedRow.Top, this.Viewport.Width, selectedRow.Height / 2.0); if (testAreaTop.Contains(mouseLocation) && anchorRow.Bottom != selectedRow.Top) { this.destinationState = DropAction.Select; this.destinationRow = selectedRow; } // This will test the right half of each of the colum headers. If the cursor is over the right half // and the rightmost part of the destination is entirely visible in the header, then it can be a // destination. Rect testAreaBottom = new Rect(this.Viewport.Left, selectedRow.Top + selectedRow.Height / 2.0, this.Viewport.Width, selectedRow.Height / 2.0); if (testAreaBottom.Contains(mouseLocation) && selectedRow.Bottom != anchorRow.Top) { this.destinationState = DropAction.Select; this.destinationRow = null; foreach (ReportRow nextRow in this.ReportGrid.Rows) { if (nextRow.Top == selectedRow.Bottom) { this.destinationRow = nextRow; } } } } } // If a valid destination was found in the search above, move the set of red arrows (the destination cursor) // over the exact spot where the row will be moved. if (this.destinationState == DropAction.Select) { this.destinationPopup.VerticalOffset = this.destinationRow == null ? this.ReportGrid.ExtentHeight : this.destinationRow.Top; this.destinationPopup.Visibility = Visibility.Visible; } else { this.destinationPopup.Visibility = Visibility.Hidden; } } else { // If the mouse isn't over the row header quadrant, a big 'X' instead of the destination cursor give the // user feedback that the row will be dropped from the viewer if they release the mouse button. this.destinationPopup.Visibility = Visibility.Hidden; this.Cursor = this.bigEx; // This will instruct the 'mouse up' action to delete the currently selected row. this.destinationState = DropAction.Delete; } // The cursor row is really a floating window, not a cursor. It needs to be moved to match the location of the // mouse. Note that the floating window doesn't have a parent, so the coordinates are in screen units. this.headerPopup.Location = mouseLocation; break; case MouseState.ButtonUp: // This will determine which cursor should be used when the button isn't pressed while moving the mouse: a horizontal // size cursor or a regular arrow cursor. If the mouse is over the bottom or top edge of the row, then the // horizontal resizing cursor is used. bool isResizingRow = false; // This will attempt to hit test the current row to see if it is a candidate for resizing. if (selectedRow != null) { // This is a 'Hit Test' for the bottom edge of the row header tile to see if the user is trying to change the // size of the row. if (selectedRow.Top + selectedRow.Height - DynamicReport.splitBorder <= mouseLocation.Y && mouseLocation.Y < selectedRow.Top + selectedRow.Height) { isResizingRow = true; } // This is a 'Hit Test' for the top edge of the row header tile to see if the user is trying to change the // size of the row. if (selectedRow.Top <= mouseLocation.Y && mouseLocation.Y < selectedRow.Top + DynamicReport.splitBorder) { isResizingRow = true; } } // Only the selection cursor is used when the header is frozen. When unfrozen, select the resizing cursor when // the mouse is over the edge of the column header. this.Cursor = this.IsHeaderFrozen || !isResizingRow ? this.selectRow : this.horizontalSplit; break; } }
/// <summary> /// Handles the mouse button being pressed. /// </summary> /// <param name="e">The event arguments.</param> protected override void OnMouseDown(System.Windows.Input.MouseButtonEventArgs e) { // The sort order is maintained in this structure. this.sortOrder = new List <SortItem>(); // This gets the location of the mouse in document coordinates. Mouse.Capture(this); // This state variable will control how the 'Mouse Move' and 'Mouse Up' event handlers interpret the user action. The // 'selectedRow' field is used as the starting point for any drag-and-drop type of operation. this.mouseState = MouseState.ButtonDown; // Evaluate the state of the keyboard. Key combinations involving the shift and control keys will alter the areas that // are selected. bool isShiftKeyPressed = ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift); bool isControlKeyPressed = ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control); // This will use the current position for an anchor unless the shift key is pressed. this.mouseDownLocation = e.GetPosition(this); if (!isShiftKeyPressed) { this.anchorPoint = this.mouseDownLocation; } // The mouse indicates which column has been selected and the anchor indicates the starting point of the selection in // an extended selection operation. ReportRow selectedRow = this.ReportGrid.Rows.FindRowAt(this.mouseDownLocation.Y); ReportRow anchorRow = this.ReportGrid.Rows.FindRowAt(this.anchorPoint.Y); Point mouseDownLocation = e.GetPosition(this); if (!isShiftKeyPressed) { IInputElement iInputElement = this.InputHitTest(mouseDownLocation); DependencyObject dependencyObject = iInputElement as DependencyObject; while (dependencyObject != null) { if (DynamicReport.GetCell(dependencyObject) != null) { Keyboard.Focus(dependencyObject as IInputElement); break; } dependencyObject = VisualTreeHelper.GetParent(dependencyObject); } } // Every cell that appears in the header canvas is considered part of the selectable header. This will collect all the // selected cells in a list while creating a rectangle that is the union of all those selected cells. this.headerCells.Clear(); foreach (ReportColumn reportColumn in this.ReportGrid.Columns) { ReportCell reportCell = selectedRow[reportColumn]; if (reportColumn.Left < this.ActualWidth) { this.headerCells.Add(reportCell); } } // If a row is selected then the position and movement of the mouse will suggest one of several gestures that need // to be interpreted: is a row being moved, is it being resized, is it being deleted or selected? The code below // will begin to interpret the input gesture. if (selectedRow != null) { // The header has two modes: when the headers are frozen, only selection operations are enabled with the mouse. // When not frozen, the rows can be moved, resized, resorted and removed. if (this.IsHeaderFrozen) { // The shift and control key extend the selection operation in the same way as Microsoft Excel. if (isShiftKeyPressed || isControlKeyPressed) { // When the shift key is pressed during row selection, every row between the last row selected // and the current row is selected. if (isShiftKeyPressed) { // In the unlikely event that the shift key was down during the setting of the anchor point, this will // create a dummy entry in the list of selected row ranges. if (this.selectedRanges.Count == 0) { List <ReportRow> reportRows = new List <ReportRow>(); reportRows.Add(selectedRow); this.selectedRanges.Add(reportRows); } // The most recent range will be replaced with a new range when the mouse is dragged around the row // headers. This has the effect of clearing the rows that are no longer selected and selecting only // the rows between the anchor and the currently selected row. List <ReportRow> lastRange = this.selectedRanges[this.selectedRanges.Count - 1]; // This will select each row between the last selected row and the one just selected. Note that the // list of rows is distinct, so duplicate rows are ignored. ReportRow firstRow = anchorRow.Top < selectedRow.Top ? anchorRow : selectedRow; ReportRow secondRow = anchorRow.Top < selectedRow.Top ? selectedRow : anchorRow; lastRange.Clear(); foreach (ReportRow reportRow in this.ReportGrid.Rows) { if (firstRow.Top <= reportRow.Top && reportRow.Top <= secondRow.Top) { lastRange.Add(reportRow); } } } // When the control key is pressed a single row is added to the range of rows selected. if (isControlKeyPressed) { // This removes any previous instance of this row in the selection. foreach (List <ReportRow> rowRange in this.selectedRanges) { if (rowRange.Contains(selectedRow)) { rowRange.Remove(selectedRow); } } // The row is added (or re-added) at the start of the range of selected rows. List <ReportRow> reportRows = new List <ReportRow>(); reportRows.Add(selectedRow); this.selectedRanges.Add(reportRows); } } else { // The row is added at the start of a new range of selected rows. List <ReportRow> reportRows = new List <ReportRow>(); // The if condition below fixes issue that allows the right click to not clear the selected rows. // Hence it allows every other condition to clear the state. if (e.RightButton != MouseButtonState.Pressed) { // A simple selection that doesn't involve the modifier keys will clear out any previously selected ranges. this.selectedRanges.Clear(); } else { // This removes any previous instance of this row in the selection. foreach (List <ReportRow> rowRange in this.selectedRanges) { if (rowRange.Contains(selectedRow)) { rowRange.Remove(selectedRow); } } } reportRows.Add(selectedRow); this.selectedRanges.Add(reportRows); } // This will select all the rows in the selected ranges of rows and remove the selection from all the // rest of the cells. SelectRows(); // This instructs the event handlers how the mouse movement is to be interpreted. this.mouseState = MouseState.Selecting; } else { // The top mouse button can either select the row or begin a resizing operation. This will perform a 'Hit // Test' to see which operation should be performed. if (e.LeftButton == MouseButtonState.Pressed) { // This is a 'Hit Test' for the bottom edge of the row header tile to see if the user is trying to change // the size of the row. If the mouse is close to the bottom edge, then the drag operation to change the // size of the tile is begun. if (selectedRow.Bottom - DynamicReport.splitBorder <= this.mouseDownLocation.Y && this.mouseDownLocation.Y < selectedRow.Bottom) { this.resizeStart = selectedRow.Bottom; this.mouseState = MouseState.ResizingRow; this.destinationRow = null; } else { // This is a 'Hit Test' for the top edge of the row header tile to see if the user is trying to change // the size of the row. Note that because the top edge really belongs to the previous row header when // resizing, that the previous row is selected for the operation. if (selectedRow.Top <= this.mouseDownLocation.Y && this.mouseDownLocation.Y < selectedRow.Top + DynamicReport.splitBorder) { this.resizeStart = selectedRow.Top; this.mouseState = MouseState.ResizingRow; foreach (ReportRow reportRow in this.ReportGrid.Rows) { if (reportRow.Bottom == selectedRow.Top) { selectedRow = reportRow; } } this.destinationRow = null; } } } // At this point, a resizing operation has been selected from the input gesture of the mouse. if (this.mouseState == MouseState.ResizingRow) { // The parent window will watch for this event to tell it how to draw the row width indicator lines. The // dimension and location of those lines are outside of this window and must be handled by the parent. if (this.ResizeMouseMove != null) { this.ResizeMouseMove(this, new ResizeRowEventArgs(selectedRow, selectedRow.Height, false)); } // This window provides quantitative feedback for the new width of the row. The offsets were arrived at // empirically from reverse engineering Excel. this.rowHeightPopup.VerticalOffset = this.mouseDownLocation.Y - 2.0; this.rowHeightPopup.Content = selectedRow.Height; this.rowHeightPopup.IsOpen = true; } else { // This will select the button momentarily for drag-and-drop and sorting operations. foreach (ReportCell reportCell in this.headerCells) { reportCell.IsSelected = true; } } } } }