The two objects to get displayed with their custom views
The first object, whichever type, gets the correct custom formatting. For the second object the custom formatting is ignored.
I think the issue is in Microsoft.PowerShell.Commands.Internal.Format.InnerFormatShapeCommand.ProcessObject()
This method is called for each object. The first time, ctx is FormattingContextState.none, and so _viewManager.Initialize is called with the first object (so), and based on that object type the custom view is selected.
On the second call, ctx is now FormattingContextState.group, so _viewManager.Initialize isn't called, and so there's never an opportunity to set the select object's custom view.
Name Value
---- -----
PSVersion 7.0.0-preview.6
PSEdition Core
GitCommitId 7.0.0-preview.6-119-g0dfeeb52034faf2696dc8844b5cbf39faba566cb
OS Microsoft Windows 10.0.18362
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
Perhaps it is the same #4594
The "related problem" in that one looks similar but different. But I've just read the issue, not investigated deeply. The one here just looks like a bug though; InnerFormatShapeCommand should be getting a custom view for each, not just the first in the list.
Is default output in your code Format-Table? Try to change to Format-List.
If I understand the issue correctly, this is sort of by design. For heterogeneous collections, the format to use is selected based on the first object (what properties to show etc.) Subsequent objects just get converted to strings (?). Doing otherwise would slow everything down and produce hard to read output because the headers would be reprinted for each change in object type. The solution is to have a common base class for the objects being emitted and write your formats against that.
It might have been part of the original design, but I think it's worth trying to do it better. 馃檪
I'm sure there are some concerns that would need to be addressed to make it work as we'd want, but simply refusing to format objects from a heterogenous collection is an undesirable solution for most folx, I would think.
The default output is pretty ugly, so what I was trying to do was format it. My Provider writes out containers and items, and I was just trying to make the container display with brackets ("[...]") in the output.
For example, I created a trivial custom provider to debug with, with this ps1xml file (notice that the first view has a
<Configuration>
<ViewDefinitions>
<View>
<Name>Class1</Name>
<ViewSelectedBy>
<TypeName>TestPSPCore.TestClass1</TypeName>
</ViewSelectedBy>
<WideControl>
<ColumnNumber>1</ColumnNumber>
<WideEntries>
<WideEntry>
<WideItem>
<PropertyName>Name</PropertyName>
<FormatString>[{0}]</FormatString> <!- <<<<<<<<<< -->
</WideItem>
</WideEntry>
</WideEntries>
</WideControl>
</View>
<View>
<Name>Class2</Name>
<ViewSelectedBy>
<TypeName>TestPSPCore.TestClass2</TypeName>
</ViewSelectedBy>
<WideControl>
<ColumnNumber>1</ColumnNumber>
<WideEntries>
<WideEntry>
<WideItem>
<PropertyName>Name</PropertyName>
</WideItem>
</WideEntry>
</WideEntries>
</WideControl>
</View>
</ViewDefinitions>
</Configuration>
Now I do a gci, which returns one of each type (named "1" and "2"), and expect to get
[1]
2
Instead I get:
1
PSPath : TestPSPCore\TestPSPCore::\2
PSParentPath : TestPSPCore\TestPSPCore::\
PSChildName : 2
PSDrive : tpsp
PSProvider : TestPSPCore\TestPSPCore
PSIsContainer : False
Name : 2
I did work around it with a common base class. That means that you have to modify the objects to have a custom formatter though. If it's necessary for the object to be designed to be formatted, then what I'd really like is to be able to set a [PSOutput] attribute or some such on a property, and then that's what's used by default to output the object. Or is there a way to cause the ToString() to be used to format the object?
It might have been part of the original design, but I think it's worth trying to do it better. 馃檪
Personally I'd really rather not see this. There aren't many good reasons to return multiple completely separate object types outside of interactively. This being a sort of hard stop has kind of cemented the best practice of only returning one thing in the community.
@MikeHillberg Instead of a WideControl, could you use a CustomControl? Custom controls and lists can be marked as "OutOfBand" which forces this separation.
<Configuration>
<ViewDefinitions>
<View>
<Name>Thing</Name>
<OutOfBand>true</OutOfBand>
<ViewSelectedBy>
<TypeName>Thing</TypeName>
</ViewSelectedBy>
<CustomControl>
<CustomEntries>
<CustomEntry>
<CustomItem>
<Text>[</Text>
<ExpressionBinding>
<PropertyName>Name</PropertyName>
</ExpressionBinding>
<Text>]</Text>
</CustomItem>
</CustomEntry>
</CustomEntries>
</CustomControl>
</View>
<View>
<Name>Thing2</Name>
<OutOfBand>true</OutOfBand>
<ViewSelectedBy>
<TypeName>Thing2</TypeName>
</ViewSelectedBy>
<CustomControl>
<CustomEntries>
<CustomEntry>
<CustomItem>
<ExpressionBinding>
<PropertyName>Name</PropertyName>
</ExpressionBinding>
</CustomItem>
</CustomEntry>
</CustomEntries>
</CustomControl>
</View>
</ViewDefinitions>
</Configuration>
([PSCustomObject]@{
PSTypeName = 'Thing'
Name = 'Thing'
},
[PSCustomObject]@{
PSTypeName = 'Thing2'
Name = 'Thing2'
})
# [Thing]
# Thing2
Personally I'd really rather not see this. There aren't many good reasons to return multiple completely separate object types outside of interactively. This being a sort of hard stop has kind of cemented the best practice of only returning one thing in the community.
That's a good point. True enough. 馃檪
Sorry, I'm not parsing that. You're saying that heterogeneous collections are bad practice?
I was thinking of it like the file system, like directories and files. But now I see that the file system provider returns both files and directories as a FileInfo, so it is homogeneous.
My solution actually was to make everything homogeneous with a common base class by which subclasses and customize their output in the provider:
abstract public class PSFormatting
{
public abstract string PSName { get; }
}
<Configuration>
<ViewDefinitions>
<View>
<Name>MyView</Name>
<ViewSelectedBy>
<TypeName>NS.PSFormatting</TypeName> <!-- <<<<<<<< -->
</ViewSelectedBy>
<WideControl>
<ColumnNumber>1</ColumnNumber>
<WideEntries>
<WideEntry>
<WideItem>
<PropertyName>PSName</PropertyName> <!-- <<<<<<<< -->
</WideItem>
</WideEntry>
</WideEntries>
</WideControl>
</View>
</ViewDefinitions>
</Configuration>
I think part of what got me down this path is that I don't like the default output I see in the FileSystem driver; the little 'd' as a directory indicator isn't obvious enough for me when I'm doing a quick look.
The issue of column headings with heterogeneous objects makes sense, but you can have objects with properties in common that don't have a common base class.
Yep that's the idea! Another thing you can do without modifying the actual original object is:
<Configuration>
<ViewDefinitions>
<View>
<Name>MyView</Name>
<ViewSelectedBy>
<TypeName>NS.PSFormattingItemBase</TypeName>
</ViewSelectedBy>
<WideControl>
<ColumnNumber>1</ColumnNumber>
<WideEntries>
<WideEntry>
<EntrySelectedBy>
<TypeName>NS.PSFormattingContainer</TypeName>
</EntrySelectedBy>
<WideItem>
<PropertyName>Name</PropertyName>
<FormatString>[{0}]</FormatString>
</WideItem>
</WideEntry>
<WideEntry>
<EntrySelectedBy>
<TypeName>NS.PSFormattingLeaf</TypeName>
</EntrySelectedBy>
<WideItem>
<PropertyName>Name</PropertyName>
</WideItem>
</WideEntry>
</WideEntries>
</WideControl>
</View>
</ViewDefinitions>
</Configuration>
That assumes something like:
public abstract class PSFormattingBase
{
public virtual string Name { get; }
}
public class PSFormattingContainer : PSFormattingBase
{
}
public class PSFormattingLeaf : PSFormattingBase
{
}
Nice, I hadn't found the EntrySelectedBy. That still requires you modify the original objects to give them a common base though. Otoh, in my case, using System.Object as a common base works, and then your solution works purely with the ps1xml.
It'd be great to call out the homogeneous guideline in the documentation and sample.
Nice, I hadn't found the EntrySelectedBy. That still requires you modify the original objects to give them a common base though. Otoh, in my case, using System.Object as a common base works, and then your solution works purely with the ps1xml.
You can also just do this:
<Configuration>
<ViewDefinitions>
<View>
<Name>MyView</Name>
<ViewSelectedBy>
<TypeName>NS.PSFormattingItemBase</TypeName>
</ViewSelectedBy>
<WideControl>
<ColumnNumber>1</ColumnNumber>
<WideEntries>
<WideEntry>
<WideItem>
<ScriptBlock>
if ($_.PSIsContainer) {
return '[{0}]' -f $_.Name
}
return $_.Name
</ScriptBlock>
</WideItem>
</WideEntry>
</WideEntries>
</WideControl>
</View>
</ViewDefinitions>
</Configuration>
A little less clean but totally fine.
It'd be great to call out the homogeneous guideline in the documentation and sample.
It's more a general rule for anything that returns objects. C# methods, functions, scripts, cmdlets, anything. Definitely agree that it should be spelled out in a help doc somewhere though, assuming it hasn't already.
Please open new issue in PowerShell Docs repo if needed.
Most helpful comment
Personally I'd really rather not see this. There aren't many good reasons to return multiple completely separate object types outside of interactively. This being a sort of hard stop has kind of cemented the best practice of only returning one thing in the community.
@MikeHillberg Instead of a
WideControl, could you use aCustomControl? Custom controls and lists can be marked as "OutOfBand" which forces this separation.