2

The task is to automate OLAP pivot table data filtering. There are some items in pivot field named sPivotFieldName I need to exclude. The code below works pretty fine.

With Worksheets(sWorksheetName).PivotTables(sPivotTableName)
    With .CubeFields(sCubeFieldName)
        .Orientation = xlRowField
        .IncludeNewItemsInFilter = True
    End With
    .PivotFields(sPivotFieldName).HiddenItemsList = vSomeItemsToExclude
End With

But the problem appears when I'm trying to change cube field ".Orientation" property's value to xlPageField. Run-time error 1004 fires each time. Here's an example:

With Worksheets(sWorksheetName).PivotTables(sPivotTableName)
    With .CubeFields(sCubeFieldName)
        .Orientation = xlPageField
        .IncludeNewItemsInFilter = True
    End With
    .PivotFields(sPivotFieldName).HiddenItemsList = vSomeItemsToExclude
End With

The reason seems to be that items of the fields placed in pagefield aren's visible as they are when placed for example in the rowfield (one can see them as row captions). Or maybe there's something else. What am I missing?

3 Answers3

1

This functionality obviously isn't available for PageFields. Seems to me a workaround is to use the .VisibleITemsList approach instead, but make sure it doesn't include the items you want to exclude.

To do this, you need to dump all the unfiltered items to a variant, loop the variant looking for the term you want to hide, and if you find it, just replace that element for some other element that you don't want to hide. (This saves you having to create a new array without that item in it).

The tricky thing is to get a list of all unfiltered items: .VisibleItemsList won't give it to you if the PivotTable doesn't have some kind of filter applied. So we need to get sneaky by making a copy of the PivotTable, making the PageField of interest a RowField, removing all other fields, and then hoovering up the complete list of items, so we know what should be visible after we remove the ones that should be hidden.

Here's a function that handles filtering no matter whether you're dealing with a RowField or a PageField and no matter whether you want to use the .VisibleItemsList to set the filter, or the .HiddenItemsList

In your particular case, you would call it like so: FilterOLAP SomePivotField, vSomeItemsToExclude, False

Function FilterOLAP(pf As PivotField, vList As Variant, Optional bVisible As Boolean = True)

    Dim vAll        As Variant
    Dim dic          As Object
    Dim sItem       As String
    Dim i           As Long
    Dim wsTemp      As Worksheet
    Dim ptTemp      As PivotTable
    Dim pfTemp      As PivotField
    Dim sPrefix     As String

    Set dic = CreateObject("Scripting.Dictionary")

    With pf
        If .Orientation = xlPageField Then
        pf.CubeField.EnableMultiplePageItems = True

            If Not pf.CubeField.EnableMultiplePageItems Then pf.CubeField.EnableMultiplePageItems = True
        End If

        If bVisible Then
            If .CubeField.IncludeNewItemsInFilter Then .CubeField.IncludeNewItemsInFilter = False
            .VisibleItemsList = vList
        Else

            If .Orientation = xlPageField Then
                ' Can't use pf.HiddenItemsList on PageFields
                ' We'll need to manipulate a copy of the PT to get a complete list of visible fields
                Set wsTemp = ActiveWorkbook.Worksheets.Add
                pf.Parent.TableRange2.Copy wsTemp.Range("A1")
                Set ptTemp = wsTemp.Range("A1").PivotTable

                With ptTemp
                    .ColumnGrand = False
                    .RowGrand = False
                    .ManualUpdate = True
                    For Each pfTemp In .VisibleFields
                        With pfTemp
                            If .Name <> pf.Name And .Name <> "Values" And .CubeField.Orientation <> xlDataField Then .CubeField.Orientation = xlHidden
                        End With
                    Next pfTemp
                    .ManualUpdate = False
                End With
                sPrefix = Left(pf.Name, InStrRev(pf.Name, ".")) & "&["
                Set pfTemp = ptTemp.PivotFields(pf.Name)
                pfTemp.CubeField.Orientation = xlRowField
                pfTemp.ClearAllFilters

                vAll = Application.Transpose(pfTemp.DataRange)
                For i = 1 To UBound(vAll)
                    vAll(i) = sPrefix & vAll(i) & "]"
                    dic.Add vAll(i), i
                Next i

                'Find an item that we know is visible
                For i = 1 To UBound(vList)
                    If Not dic.exists(vList(i)) Then
                        sItem = vList(i)
                        Exit For
                    End If
                Next i

                'Change any items that should be hidden to sItem
                For i = 1 To UBound(vList)
                    If dic.exists(vList(i)) Then
                        vAll(dic.Item(vList(i))) = sItem
                    End If
                Next i

                .VisibleItemsList = vAll

                Application.DisplayAlerts = False
                wsTemp.Delete
                Application.DisplayAlerts = True

            Else
                If Not .CubeField.IncludeNewItemsInFilter Then .CubeField.IncludeNewItemsInFilter = True
                .HiddenItemsList = vList
            End If
        End If

    End With


End Function
jeffreyweir
  • 4,668
  • 1
  • 16
  • 27
1

I hope it's not too late for me to contribute an answer, just for posterity's sake.

If you look at the PivotTable.MDX property on any OLAP PivotTable, you can see the MDX query which Excel is actually using behind the scenes to populate the data which shows up in the PivotTable. Inspired somewhat by this observation, I thought to myself: shouldn't it be possible to be even sneakier, by (a) creating an ADODB connection with the same connection string which the PivotCache uses, (b) putting together an appropriate MDX query ourselves, and (c) reading the result directly into an array in VBA, to which we can then assign the PivotField.VisibleItemsList property?

Benefits of this approach include...

  • Avoiding the overhead and awkwardness of having to create & destroy a temporary PivotTable to get the full list of items;
  • Handling OLAP PivotFields correctly which have more than 1,048,575 members--putting these on Rows with the temporary-PivotTable approach would cause an error, as the PivotTable would exceed the maximum number of rows on a Worksheet; and,
  • Using an MDX query that's a bit faster and more efficient than the one Excel would most likely use by default.

Without further ado (or perhaps with further ADO? hehe), here's the VBA subroutine I came up with.

' Filter a PivotField in an OLAP PivotTable on either Visible or Hidden items.
Public Sub FilterOLAPPivotField(oPF As PivotField, vItems As Variant, _
        Optional ByVal bVisible As Boolean = True)

    Dim dictItems As Object
    Dim i As Long
    Dim sConn As String, sConnItems() As String
    Dim sCatalog As String
    Dim sQuery As String
    Dim oConn As Object
    Dim oRS As Object
    Dim vRecordsetRows As Variant
    Dim dictVisibleItems As Object

'   In case something fails while we still have the ADODB Connection or Recordset 
'   open, this ensures the subroutine will "fail gracefully" and still close them.
'   Feel free to add some more error handling if you like!

    On Error GoTo Fail

'   Turn on "checkbox mode" for selecting more than one filter item, for convenience.

    oPF.CubeField.EnableMultiplePageItems = True

'   If filtering on Visible items: then we just need to set the PivotField's 
'   VisibleItemsList property to the vItems array, and we can skip the rest.

    If bVisible Then
        oPF.VisibleItemsList = vItems
        Exit Sub
    End If

'   All the rest of this subroutine is just for the case where we want our vItems 
'   to be the *Hidden* items, i.e. so everything *but* those items is visible.

'   Read vItems into a Scripting.Dictionary. This is for convenience; we want to use 
'   its Exists method later. We only really care about the Keys; the Item:=True 
'   is just a dummy.

    Set dictItems = CreateObject("Scripting.Dictionary")
    For i = LBound(vItems) To UBound(vItems)
        dictItems.Add Key:=vItems(i), Item:=True
    Next i

'   Get the connection string from the PivotCache of the PivotField's parent PivotTable 
'     (This assumes it is an OLEDB connection.)
'   The connection string is needed to make a separate connection to the server 
'   with ADODB. It also contains the Initial Catalog, which we also need.

    sConn = Replace$(oPF.Parent.PivotCache.Connection, "OLEDB;", vbNullString, Count:=1)
    sConnItems = Split(sConn, ";")
    For i = LBound(sConnItems) To UBound(sConnItems)
        If sConnItems(i) Like "Initial Catalog=*" Then
            sCatalog = "[" & Split(sConnItems(i), "=")(1) & "]"
            Exit For
        End If
    Next i

'   Construct an MDX query to send to the server, which just gets the UNIQUE_NAME of 
'   all the members in the hierarchy we're interested in.

    sQuery = Join$(Array( _
        "WITH MEMBER [Unique Name] AS", _
        oPF.CubeField.Name & ".CURRENTMEMBER.UNIQUE_NAME", _
        "SELECT [Unique Name] ON 0,", _
        oPF.Name, "ON 1 FROM", _
        sCatalog _
    ))

'   Using ADODB, get the result of the query, and dump it into a Variant array.

    Set oConn = CreateObject("ADODB.Connection")
    Set oRS = CreateObject("ADODB.Recordset")
    oConn.Open sConn
    oRS.Open sQuery, oConn
    vRecordsetRows = oRS.GetRows()

'   The Recordset rows are a multidimensional array with 2 columns: column 0 contains 
'   the member captions, and column 1 (which is the one we want) contains the unique names.
'   So we loop through the result, adding any member which was *not* in vItems to 
'   a new Scripting.Dictionary.

    Set dictVisibleItems = CreateObject("Scripting.Dictionary")
    For i = 0 To oRS.RecordCount - 1
        If Not dictItems.Exists(vRecordsetRows(1, i)) Then
            dictVisibleItems.Add Key:=vRecordsetRows(1, i), Item:=True
        End If
    Next i

'   dictVisibleItems.Keys now contains all member which were *not* in vItems. 
'   All that remains is to set the PivotField's VisibleItemsList to this array!

    oPF.VisibleItemsList = dictVisibleItems.Keys

Fail:
'   Last but not least: don't forget to close the ADODB Connection and Recordset.
'   If we got to this point normally, then (despite the 'Fail' label) we just close 
'   them uneventfully and end.

'   If we jumped here because of an error, then we see a MsgBox at this point, but the 
'   subroutine will try to "fail gracefully" and still close the Connection & Recordset.

'   Just in case we somehow ended up down here via an error raised *before* the 
'   Connection or Recordset was ever open, we also have "On Error Resume Next". 
'   Otherwise, the Close method itself might raise an error, sending us back to 'Fail'
'   and trapping the subroutine in an infinite loop!

    If Err Then
        MsgBox "Something went horribly wrong", vbCritical, "Error"
        Err.Clear
    End If

    On Error Resume Next
    oRS.Close
    oConn.Close

End Sub

If you're interested in using it in your own Workbook, then just copy it into a standard module, and call it with the relevant arguments.

For example: FilterOLAPPivotField(ActiveCell.PivotField, Items, False) would filter the PivotField under the active cell so that it contains all items except those in the Items array.

An oddity I observed while testing this on my machine: sometimes, CubeField.EnableMultiplePageItems seems to think it's a read-only property if I have just opened a Workbook with a PivotField I'm trying to manipulate. Because the subroutine writes to this property, this can cause it to fail. Clicking once in the UI to open the filter dropdown always seems to make the problem go away. Not sure exactly what's behind this... maybe the PivotCache is not loading until I actually interact with the PivotTable? If anyone else has some insight, I'd be quite interested to learn about what causes this.


One last side note: if you plan on doing some manual finagling of a bunch of PivotFields on an existing Excel Workbook, then one thing you might also consider would be to put a button on your Quick Access Toolbar which just inverts all the filters on the PivotField under the active cell, i.e. which includes everything that's currently filtered & filters everything that's currently included. Or, you might want to have a UserForm with a CommandButton which does something similar. You could use the above subroutine to create such a button, by having another sub which calls it, like so:

' Invert the filters on the OLAP PivotField under the active cell.
Public Sub btnInvertOLAPPivotFieldFilter_Click()
    Dim oPF As PivotField
    Set oPF = ActiveCell.PivotField
    oPF.CubeField.EnableMultiplePageItems = True
    FilterOLAPPivotField oPF, oPF.VisibleItemsList, False
End Sub
indnwkybrd
  • 139
  • 2
0

Someone please, show me example how it works((

Dim pt As PivotTable
Dim pf As PivotField


Set pt = ActiveSheet.PivotTables("Сводная таблица2")
Set pf = pt.PivotFields("[груп бай].[Название клиента].[Название клиента]")
wList = "[груп бай].[Название клиента].&[ООО ""Сеть автоматизированных пунктов выдачи""]"
FilterOLAP(pf, wList, FAlse)

debuging here

> If .Name <> pf.Name And .Name <> "Values" And .CubeField.Orientation
> <> xlDataField Then .CubeField.Orientation = xlHidden
  • 1
    This does not really answer the question. If you have a different question, you can ask it by clicking [Ask Question](https://stackoverflow.com/questions/ask). To get notified when this question gets new answers, you can [follow this question](https://meta.stackexchange.com/q/345661). Once you have enough [reputation](https://stackoverflow.com/help/whats-reputation), you can also [add a bounty](https://stackoverflow.com/help/privileges/set-bounties) to draw more attention to this question. - [From Review](/review/late-answers/30907270) – Ike Jan 26 '22 at 15:36