I was hoping someone can help me figure this issue out. The combox seems to only list an item as individual characters when the binding source only has one value. If it is two or more, it lists the items properly.
Here's are two links with people experiencing similar issues.
Link 1
Link 2
<DataTemplate>
<ComboBox ItemsSource="{Binding 'Clusters'}"
SelectedItem="{Binding Path='TargetCluster', Mode=TwoWay}"
Width="145"
/>
Here's the item source
$vCenters = #()
Foreach ($vCenter in $VDIEnvironments) {
$vCenter |
Add-Member -MemberType NoteProperty -Name TargetCluster -Value (
$clusters | ? VCName -like $vCenter.Name
)[0].Name -Force
$vCenter |
Add-Member -MemberType NoteProperty -Name Clusters -Value $(
$clusters | ? VCName -like $vCenter.Name
).Name -Force
$vCenter |
Add-Member -MemberType NoteProperty -Name TargetDatastore -Value $(
$datastores | ? VCName -like $vCenter.Name | Sort-Object -Descending FreeSpaceMB
)[0].Name -Force
$vCenter |
Add-Member -MemberType NoteProperty -Name Datastores -Value $(
$datastores | ? VCName -like $vCenter.Name
).Name -Force
$vCenter |
Add-Member -MemberType NoteProperty -Name TargetPortgroup -Value (
$portgroups | ? VCName -like $vCenter.Name | Sort-Object -Descending NumPorts
)[0].Name -Force
$vCenter |
Add-Member -MemberType NoteProperty -Name Portgroups -Value $(
$portgroups | ? VCName -like $vCenter.Name
).Name -Force
$vCenters += $vCenter
}
Filling the datagridvie
$SelectedVCenters = $VCenters |
Where-Object Env -like $WPFboxEnvironment.Text |
Where-Object Datastores -ne $Null
$SelectedVCenters | ForEach-Object {
$WPFboxSrcVCenter.Items.Add($_.Name)
$WPFlistTgtVCenters.Items.Add($_)
$WPFlistTgtVCenters.SelectedItems.Add($_)
}
Thanks for the clarification. My colleague and I ended up adding an empty cluster to all vCenters to remedy the issue. Not the best solution but it works and solve two problems that we had.
$clusters = Get-Cluster | Select-Object Name, Uid
$clusters | ForEach-Object {
$_ | Add-Member -MemberType NoteProperty -Name VCName -Value $_.Uid.split('#')[1].split(':')[0]
}
$uniqueVCs = $clusters | select VCName | sort VCName -Unique
foreach ($VC in $uniqueVCs) {
$clusterplaceholder = [pscustomobject]#{
'Name' = "Select a cluster"
'Uid' = " "
'VCName' = $VC.VCName
}
$clusters += $clusterplaceholder
}
$clusters = $clusters | sort VCName, Uid
This normally happens when you bind an ItemsControl.ItemsSource to a string. The ItemsControl internally accesses a copy of the collection bound to the ItemsSource by index, because it has to create a container for each data item (ItemContainerGenerator) in order to render the data as a Visual object.
Since string implements an indexer like
public char this[int index] { get; }
, it is accessible by index like a collection or an array.
Now, when binding a string to ItemsControl.ItemsSource, the string is copied to the ItemsControl.Items collection and passed on to the internal ItemContainerGenerator, which is responsible for the creation of the visual items that are finally rendered as a visual representation of the data. This ItemContainerGenerator is treats the string value like a collection (since string implements IEnumerable) and accesses it by index. Because of the implemented indexer, the string will return its underlying characters and then the generator creates a container for each. This is why the string looks split up.
Make sure you are always binding to a collection of string, but never to a string directly to avoid this behavior.
The view model that exposes the string value and a collection of strings for binding
class ViewModel : INotifyPropertyChanged
{
private string stringValue;
public string StringValue
{
get => this.stringValue;
set
{
this.stringValue= value;
OnPropertyChanged();
}
}
private ObservableCollection<string> stringValues;
public ObservableCollection<string> StringValues
{
get => this.stringValues;
set
{
this.stringValues= value;
OnPropertyChanged();
}
}
}
MainWindow.xaml where the DataContext is the ViewModel class
<!-- This ComboBox will display the single characters of the string value (each item is a character)-->
<ComboBox x:Name="comboBox" ItemsSource="{Binding StringValue}" />
<!-- This ComboBox will display the strings of the StringValues collection (each item is a complete string) -->
<ComboBox x:Name="comboBox" ItemsSource="{Binding StringValues}" />
The displayed items of a ComboBox (or ItemsControl in general) are actually containers. A container is the visual representation of the data and is complex like a UserControl. The container has Border, Background, Padding, Margin etc. It's a Visual that is composed of other Visuals (or controls). The string alone cannot be rendered like this (having a font, font color, background, etc).
Therefore the ItemsControl must create a visual container for each data object.
This is done by the ItemsControl.ItemsPanel which actually uses the ItemContainerGenerator to accomplish this. So, internally the ComboBox (or the ItemsControl.ItemsPanel) accesses the bound collection of ItemsControl.Items in order to create the container like this:
IItemContainerGenerator generator = this.ItemContainerGenerator;
GeneratorPosition position = generator.GeneratorPositionFromIndex(0);
using (generator.StartAt(position, GeneratorDirection.Forward, true))
{
DependencyObject container = generator.GenerateNext();
generator.PrepareItemContainer(container);
}
As you can see the generator accesses the items by index. Under the hood the generator accesses a copy of ItemsControl.Items (ItemContainerGenerator.ItemsInternal) to retrieve the data that should be hosted by the container. This somehow (inside the generator) looks like:
object item = ItemsInternal[position];
Since string implements an indexer you can access a string also like an array:
var someText = "A String";
char firstCharacter = someText[0]; // References 'A' from "A String"
So when looking at the container generator code from above you now can now understand the effect the lines
GeneratorPosition position = generator.GeneratorPositionFromIndex(0);
and
generator.StartAt(position, GeneratorDirection.Forward, true)
have on a string where position is the actual item index: it retrieves character by character to map them to a container.
This is a simplified explanation of how the ItemsControl handles the source collection.
Related
Warning: I am looking to do this in PowerShell v2 (sorry!).
I would like to have a custom object (possibly created as a custom type) that has an array property. I know how to make a custom object with "noteproperty" properties:
$person = new-object PSObject
$person | add-member -type NoteProperty -Name First -Value "Joe"
$person | add-member -type NoteProperty -Name Last -Value "Schmoe"
$person | add-member -type NoteProperty -Name Phone -Value "555-5555"
and I know how to make a custom object from a custom type:
Add-Type #"
public struct PersonType {
public string First;
public string Last;
public string Phone;
}
"#
$person += New-Object PersonType -Property #{
First = "Joe";
Last = "Schmoe";
Phone = "555-5555";
}
How can I create a custom object with a type that includes an array property? Something like this hash table, but as an object:
$hash = #{
First = "Joe"
Last = "Schmoe"
Pets = #("Fluffy","Spot","Stinky")
}
I'm pretty sure I can do this using [PSCustomObject]$hash in PowerShell v3, but I have a need to include v2.
Thanks.
When you use Add-Member to add your note properties, the -Value can be an array.
$person | add-member -type NoteProperty -Name Pets -Value #("Fluffy","Spot","Stinky")
If you want to create the properties as a hashtable first, like your example, you can pass that right into New-Object as well:
$hash = #{
First = "Joe"
Last = "Schmoe"
Pets = #("Fluffy","Spot","Stinky")
}
New-Object PSObject -Property $hash
Your PersonType example is really written in C#, as a string, which gets compiled on the fly, so the syntax will be the C# syntax for an array property:
Add-Type #"
public struct PersonType {
public string First;
public string Last;
public string Phone;
public string[] Pets;
}
"#
Say I have a PowerShell array $Sessions = #() which I am going to fill with PSCustomObjects. How can I add a custom property to the array itself? E.g. so I can have $Sessions.Count which is built-in and $Sessions.Active which I want to set to the active session count.
I know that I can add properties to PSCustomObjects (in a dirty way) with
$MyCustomObject = "" | Select-Object Machine, UserName, SessionTime
but though doing so on an array would not result in the property being added.
So how can I achieve my goal? Is there any way to create a custom array?
The answer to your question as stated would be to just use Add-Member on the array object.
Add-Member -InputObject $sessions -MemberType NoteProperty -Name "State" -Value "Fabulous"
Adding a property to each element after you created the object is similar.
$sessions | ForEach-Object{
$_ | Add-Member -MemberType NoteProperty -Name "State" -Value "Fabulous"
}
This of course comes with a warning (that I forgot about). From comments
Beware, though, that appending to that array ($sessions += ...) will replace the array, thus removing the additional property.
Ansgar Wiechers
Depending on your use case there are other options to get you want you want. You can save array elements into distinct variables:
# Check the current object state
$state = $object.Property .....
# Add to the appropriate array.
if($state -eq "Active"){
$activeSessions += $object
} else {
$inactiveSessions += $object
}
Or you could still store your state property and post process with Where-Object as required:
# Process each inactive session
$sessions | Where-Object{$_.State -eq "Active"} | ForEach-Object{}
To avoid the destroying / recreating array issue, which can be a performance hog, you could also use an array list instead.
$myArray = New-Object System.Collections.ArrayList
Add-Member -InputObject $myArray -MemberType ScriptMethod -Name "NeverTellMeTheOdds" -Value {
$this | Where-Object{$_ % 2 -ne 0}
}
$myArray.AddRange(1..10)
$myArray.NeverTellMeTheOdds()
Notice that the array had its member added then we added its elements.
As Matt commented, you can use the Add-Member on an enumerable type by supplying it as a positional argument to the -InputObject parameter.
To allow for resizing after adding the new property, use a generic List instead of #():
$list = [System.Collections.Generic.List[psobject]]::new()
$list.AddRange(#(
[pscustomobject]#{SessionId = 1; Active = $true}
[pscustomobject]#{SessionId = 2; Active = $false}
[pscustomobject]#{SessionId = 3; Active = $true}
) -as [psobject[]])
Add-Member -InputObject $list -MemberType ScriptProperty -Name ActiveSessionCount -Value {
return #($this |? Active -eq $true).Count
}
Now you can retrieve the active session count easily:
PS C:\> $list.ActiveSessionCount
2
I have an array of custom objects:
$report = #()
foreach ($person in $mylist)
{
$objPerson = New-Object System.Object
$objPerson | Add-Member -MemberType NoteProperty -Name Name -Value $person.Name
$objPerson | Add-Member -MemberType NoteProperty -Name EmployeeID
$objPerson | Add-Member -MemberType NoteProperty -Name PhoneNumber
$report += $objPerson
}
Note that I haven't set values for the last two properties. The reason I've done this is because I'm trying to produce a matrix where I'll easily be able to see where these are blanks (although I could just set these to = "" if I have to).
Then, I want to iterate through a second dataset and update these values within this array, before exporting the final report. E.g. (this bit is pretty much pseudo code as I have no idea how to do it:
$phonelist = Import-Csv .\phonelist.csv
foreach ($entry in $phonelist)
{
$name = $entry.Name
if ($report.Contains(Name))
{
# update the PhoneNumber property of that specific object in the array with
# another value pulled out of this second CSV
}
else
{
# Create a new object and add it to the report - don't worry I've already got
# a function for this
}
}
I'm guessing for this last bit I probably need my if statement to return an index, and then use that index to update the object. But I'm pretty lost at this stage.
For clarity this is a simplified example. After that I need to go through a second file containing the employee IDs, and in reality I have about 10 properties that need updating all from different data sources, and the data sources contain different lists of people, but with some overlaps. So there will be multiple iterations.
How do I do this?
I would read phonelist.csv into a hashtable, e.g. like this:
$phonelist = #{}
Import-Csv .\phonelist.csv | ForEach-Object { $phonelist[$_.name] = $_.number }
and use that hashtable for filling in the phone numbers in $report as you create it:
$report = foreach ($person in $mylist) {
New-Object -Type PSObject -Property #{
Name = $person.Name
EmployeeID = $null
PhoneNumber = $phonelist[$person.Name]
}
}
You can still check the phone list for entries that are not in the report like this:
Compare-Object $report.Name ([array]$phonelist.Keys) |
Where-Object { $_.SideIndicator -eq '=>' } |
Select-Object -Expand InputObject
I would iterate over the $phonelist two times. The first time, you could filter all phone entities where the name is in your $myList and create the desired object:
$phonelist = import-cse .\phonelist.csv
$report = $phonelist | Where Name -in ($mylist | select Name) | Foreach-Object {
[PSCustomObject]#{
Name = $_.Name
PhoneNumber = $_.PhoneNumber
EmployeeID = ''
}
}
The second time you filter all phone entities where the name is not in $myList and create the new object:
$report += $phonelist | Where Name -NotIn ($mylist | select Name) | Foreach-Object {
#Create a new object and add it to the report - don't worry I've already got a function for this
}
I have created a custom object called $info and moving it to an array $arr ,
How is it possible to remove one member along with its all properties ?
My script:
Get-Process | ForEach-Object{
$info = New-Object -TypeName PSObject
$info | Add-Member -Type NoteProperty -Name Process -Value $_.processname
$info | Add-Member -Type NoteProperty -Name ID -Value $_.id
$arr += $info
}
$arr | ft -AutoSize
The result looks like this :
Process ID
------- --
ApplicationFrameHost 38556
AppVShNotify 9792
armsvc 2336
atieclxx 6944
atiesrxx 1844
audiodg 59432
CcmExec 3988
chrome 46068
How can I remove one particular member for example "audiodg 59432" gets removed
audiodg 59432
Your terminology is a bit incorrect here. A member is on an individual object. When you use Add-Member above you're adding properties to each individual object, then you're returning an array of objects.
You're asking how to remove an individual object from the array.
In PowerShell you cannot remove an item from an array. You could instead filter the array based on some criteria and create a new one:
$newArr = $arr | Where-Object { $_.Name -ne 'audiodg' }
# or
$newArr = $arr | Where-Object { $_.ID -ne 59432 }
I'm having dificulties working with an array of custom objects and matching it with a variable.
I have a variable: $CmbCust.SelectedItem (currently selected item in a WPF form)
Custom-Object and the creation of my items in the combobox:
$CustomerFileArray = #()
ForEach ($c in (Get-ChildItem $ProgramRoot\Customers -Filter Customer-*.xml | sort Name -descending)) {
$XmlCustomer = [xml](Get-Content $ProgramRoot\Customers\$c)
if ($XmlCustomer.Office365.Customer.Name -eq "") {
$CustomerName = "- Geen naam"
}
Else {
$CustomerName = $XmlCustomer.Office365.Customer.Name
}
$CustomerItem = New-Object PSObject
$CustomerItem | Add-Member -type NoteProperty -Name 'Name' -Value $CustomerName
$CustomerItem | Add-Member -type NoteProperty -Name 'File' -Value $c
$CustomerFileArray += $CustomerItem
[void] $CmbCust.Items.Add($CustomerName)
}
$CmbCust.SelectedItem = $XmlOffice365.Office365.Customer.Name
My question is, how can I match the value in $CmbCust.SelectedItem with my Array $CustomerFileArray's File property
The action I would like to do is to get a path of the selected item to remove it. I've Googled and came up with:
$RemoveFile = #()
$RemoveFile | where {$CustomerFileArray.ContainsKey($_.CmbCust.SelectedItem)}
Remove-Item $ProgramRoot\Customers\$RemoveFile -Force
But that doesn't seem to work...
Thanks in advance!
Since the SelectedItem property contains a string found in the Name property of one of the items in $CustomerFileArray, you should apply Where-Object to $CustomerFileArray like so:
$CustomerFileArray |Where-Object {$_.Name -eq $CmbCust.SelectedItem} |Select-Object -ExpandProperty File
This will return the FileInfo object you originally assigned to the File property of the corresponding object in the array