none
Powershell WPF doubleclick event from programmatically generated datagrid cell RRS feed

  • Question

  • Hello, I am attempting to create a doubleclick event .Add_DoubleClick({function})

    The missing code generates links to the location of log files.

    This code can generate up to 5 datagrids.

    The datagrids have 3 columns each but column0 is the only important one.

    When I run $syncHash I'm having trouble finding the dynamically generated controls.

    $syncHash.datagrid0.Tables[0]|select-object -property * doesn't give anything after the datagrid is filled.

    Secondly, the doubclick event isn't firing and is generating an error

    You cannot call a method on a null-valued expression.
    At line:304 char:7
    +       $syncHash.$dgName.Add_DoubleClick({
    +       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
        + FullyQualifiedErrorId : InvokeMethodOnNull

    Which leads me to believe the control is not really instantiating however, the grids do populate. 


    $Global:syncHash = [hashtable]::Synchronized(@{})
    $syncHash.host = $host
    $newRunspace =[runspacefactory]::CreateRunspace()
    $newRunspace.ApartmentState = "STA"
    $newRunspace.ThreadOptions = "ReuseThread"
    $newRunspace.Open()
    $newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
    #endregion Global Variables
    Add-Type -AssemblyName PresentationFramework, System.Drawing, System.Windows.Forms, WindowsFormsIntegration
    $psCmd = [PowerShell]::Create().AddScript({
      #region XAML Graphics processing
      [xml]$xaml = @"
    <Window
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:sys="clr-namespace:System;assembly=mscorlib"
      xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
      Title="MainWindow" Height="600" Width="1100">
      <Grid Name="G1">
        <Grid.RowDefinitions>
            <RowDefinition Height="25" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <DockPanel HorizontalAlignment="Center" LastChildFill="False" Grid.Row="0" >
          <Button Name ="Search" Content="Search" Padding="2"/>
          <TextBox Name="textbox" Width="60" Padding="2"/>
        </DockPanel>
        <Grid Name="G2" Grid.Row="1" />
      </Grid>
    </Window>
    "@
     
      $reader=(New-Object System.Xml.XmlNodeReader $xaml)
      $syncHash.Window=[Windows.Markup.XamlReader]::Load($reader)
      [xml]$xaml = $xaml
    
      $xaml.SelectNodes("//*[@Name]") | ForEach-Object {
        $syncHash.Add($_.Name,$syncHash.Window.FindName($_.Name))
      }
      #endregion XAML Graphics processing
      #region functions
      function Get-Logs {
        param($links)
    
         #disable Search Button
          $syncHash.Search.IsEnabled=$true
     
        $colWidth = [Math]::Round(100/$links.Count,0)
        
        For ($i=0; $i -lt $links.Count; $i++) {
          #log location
          $logloc = $links[$i]
     
          #label      
          $lbName = 'label{0}' -f $i
          $newLabel = New-Object System.Windows.Controls.Label
          $newLabel.Name = $lbName
          $newLabel.FontSize = "10"
          $newLabel.Content = $logloc
          $newLabel.Foreground="blue"
    
          #datagrid
          $dgName = 'datagrid{0}' -f $i
          $newDataGrid = new-object System.Windows.Controls.DataGrid
          $newDataGrid.Name = $dgName
          $newDataGrid.ColumnWidth = "*"
          $newDataGrid.IsReadOnly = $true
    
          #dockpanel
          $dpName = "DockPanel{0}" -f $i
          $newDockPanel = New-Object System.Windows.Controls.DockPanel
          $newDockPanel.Children.Insert(0,$newLabel)
          $newDockPanel.Children.Insert(1,$newDataGrid)
          [System.Windows.Controls.DockPanel]::SetDock($newLabel,'Top')
    
          [System.Windows.Controls.Grid]::SetColumn($newDockPanel,$i)
          $syncHash.G2.AddChild($newDockPanel)
    
          [System.Data.DataSet]$DS = New-Object System.Data.DataSet
          $DS.Tables.Add($dt)
          $newdatagrid.ItemsSource = $DS.Tables[0].DefaultView
    
          $syncHash.$dgName.Add_DoubleClick({
            $file = $syncHash.$dgName.Rows[$rowIndex].Cells[0].value
            $Address = $logloc + $file
            [System.Diagnostics.Process]::Start("$Address")
          })
          
          #Enable Search Button
          $syncHash.Search.IsEnabled=$true
        }
      }
      #endregion Functions
      #region Events
      $syncHash.Search.Add_Click({
        $links = $syncHash.Search.Text
        $syncHash.G2.ColumnDefinitions.Clear()
        $syncHash.Window.Dispatcher.invoke([action]{Get-Logs -Links $Links},"Background")
      })
    
      $syncHash.Window.Add_Closed({$syncHash.Window.Close()})
    
    [System.Windows.Forms.Integration.ElementHost]::EnableModelessKeyboardInterop($syncHash.Window)
    
      $syncHash.Window.ShowDialog()
      #endregion Events
    })
    
    $psCmd.Runspace = $newRunspace
    $data = $psCmd.BeginInvoke()
    $Error
    
    foreach ($runerr in $psCmd.Streams.Error) {
      $runerr | Select-object -Property *
    }


    Thank you


    • Edited by srvSteve Tuesday, September 10, 2019 7:28 PM
    Tuesday, September 10, 2019 7:27 PM

Answers

  • The synchash will display all names found.  Names embedder into a form programmatically will not be part of the synchash.  Only names in the XAML are findable with the code.

    You would do best to add all of the controls in the XAML which will help avoid this issue.

    You can also try adding the code directly to the control;

    $dgName = 'datagrid{0}' -f $i
    $newDataGrid = new-object System.Windows.Controls.DataGrid
    $newDataGrid.Name = $dgName
    $newDataGrid.ColumnWidth = "*"
    $newDataGrid.IsReadOnly = $true
    $newDataGrid.add_DoubleClick({
        $file = $this.Rows[$rowIndex].Cells[0].value
        $Address = $logloc + $file
        [System.Diagnostics.Process]::Start($Address)
    })
    

    Avoid the bad habit of placing quotes around variables that are already strings.


    \_(ツ)_/


    • Edited by jrvModerator Tuesday, September 10, 2019 7:52 PM
    • Marked as answer by srvSteve Wednesday, September 11, 2019 6:54 PM
    Tuesday, September 10, 2019 7:51 PM
    Moderator

All replies

  • The synchash will display all names found.  Names embedder into a form programmatically will not be part of the synchash.  Only names in the XAML are findable with the code.

    You would do best to add all of the controls in the XAML which will help avoid this issue.

    You can also try adding the code directly to the control;

    $dgName = 'datagrid{0}' -f $i
    $newDataGrid = new-object System.Windows.Controls.DataGrid
    $newDataGrid.Name = $dgName
    $newDataGrid.ColumnWidth = "*"
    $newDataGrid.IsReadOnly = $true
    $newDataGrid.add_DoubleClick({
        $file = $this.Rows[$rowIndex].Cells[0].value
        $Address = $logloc + $file
        [System.Diagnostics.Process]::Start($Address)
    })
    

    Avoid the bad habit of placing quotes around variables that are already strings.


    \_(ツ)_/


    • Edited by jrvModerator Tuesday, September 10, 2019 7:52 PM
    • Marked as answer by srvSteve Wednesday, September 11, 2019 6:54 PM
    Tuesday, September 10, 2019 7:51 PM
    Moderator
  • Your $syncHash observation makes sense. I am able to clear columndefinitions because they are a property of G2 which can be manipulated by $syncHash. Perhaps I've gone as far down the rabbit hole as is possible with this.

    Unfortunately, that did not work:

    Method invocation failed because [System.Windows.Controls.DataGrid] does not contain a method 
    named 'Add_DoubleClick'.
    At line:290 char:7
    +       $newDataGrid.Add_DoubleClick({
    +       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
        + FullyQualifiedErrorId : MethodNotFound

    I chose to dynamically create the grids because the log locations may be in as few as 2 or as many as 5 locations.

    I could pre-stage 10 columnsdefinitions with width=0 and expand / reset width and contents as necessary.

    In a separate GUI I do that with a textbox which "appears" as necessary.

    Tuesday, September 10, 2019 8:09 PM
  • You cannot add an event that is not supported by the control.

    Here are all of the events for the datagrid: https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.datagrid?view=netframework-4.8#events 

    THe correct event is "MouseDoubleClick".


    \_(ツ)_/

    Tuesday, September 10, 2019 8:21 PM
    Moderator
  • The synchash will display all names found.  Names embedder into a form programmatically will not be part of the synchash.  Only names in the XAML are findable with the code.

    You would do best to add all of the controls in the XAML which will help avoid this issue.

    You can also try adding the code directly to the control;

    $dgName = 'datagrid{0}' -f $i
    $newDataGrid = new-object System.Windows.Controls.DataGrid
    $newDataGrid.Name = $dgName
    $newDataGrid.ColumnWidth = "*"
    $newDataGrid.IsReadOnly = $true
    $newDataGrid.add_DoubleClick({
        $file = $this.Rows[$rowIndex].Cells[0].value
        $Address = $logloc + $file
        [System.Diagnostics.Process]::Start($Address)
    })

    Avoid the bad habit of placing quotes around variables that are already strings.


    \_(ツ)_/


    I'm still working on 

        $file = $this.Rows[$rowIndex].Cells[0].value
        $Address = $logloc + '/' + $file

    Cannot index into a null array.
    At line:310 char:9
    +         $file = $this.Rows[$rowIndex].Cells[0].value
    +         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
        + FullyQualifiedErrorId : NullArray
     
    Exception calling "Start" with "1" argument(s): "Value cannot be null.
    Parameter name: startInfo"
    At line:312 char:9
    +         [System.Diagnostics.Process]::Start($Address)
    +         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
        + FullyQualifiedErrorId : ArgumentNullException

    So I cast the var $Address into a popup to see what it's sending...  

    It's just sending the forward slash

    Any idea why it's not using either var?


    • Edited by srvSteve Wednesday, September 11, 2019 2:53 PM
    Wednesday, September 11, 2019 2:51 PM
  • Your variables are not scoped properly and you likely have not referenced the correct parts of the grid to get you contents.

    It is not possible for us to debug your code as there is no way to run your code other than on your system.


    \_(ツ)_/

    Wednesday, September 11, 2019 5:24 PM
    Moderator
  • All true.

    $this is the only variable that is received by the doubleclick due to scope.

    I am working through the grid selection. I have experience with this so it shouldn't take long.

    The scoping will require some work and may just work out better to place a hidden column with the full concatenated url.

    I have marked this answered

    Thank you

    Wednesday, September 11, 2019 6:57 PM
  • Here is the correct way to do this event:

    $Button3_Click = {
        $textbox11.Clear()
        $User = Get-ADUser $TextBox1.Text -Properties *
        $textbox2.Text = $user.givenname
        $textbox3.Text = $user.displayname
        $textbox4.Text = $user.DistinguishedName
        $textbox5.Text = $user.Surname
        $textbox6.Text = $user.EmployeeID
        $combobox1.Text = $user.Office
        if ($user.AccountExpirationDate){ # tests for null
            $textbox8.Text = $user.AccountExpirationDate.ToString('dd/MM/yyyy HH:mm:ss')
        } else {
            $textbox8.Text = 'No account expiration date set'
        }
    }

    Note that learning basic PowerShell correctly will save you a lot of pain and unnecessary code.  I recomemnt starting with a good book on PS that teaches from the beginning.  Three chapters in and you will be almost a wiz compared to most who inssist on guesing.

    Here is the current best book written by members of the PowerShell design and development team: https://www.amazon.com/Windows-PowerShell-Action-Bruce-Payette/dp/1633430294


    \_(ツ)_/


    • Edited by jrvModerator Wednesday, September 11, 2019 7:04 PM
    Wednesday, September 11, 2019 7:04 PM
    Moderator
  • All true.

    $this is the only variable that is received by the doubleclick due to scope.

    I am working through the grid selection. I have experience with this so it shouldn't take long.

    The scoping will require some work and may just work out better to place a hidden column with the full concatenated url.

    I have marked this answered

    Thank you

    '$this" is a passed  argument of an event and is the control that generated the event.  It is not available in a function called by the event.   The "$_" argument is the "EventArgs" for that event and contains the extra information around the event.  Look at the docs for the event to see the defined arguments for that event.

    Every control has a "Tag" property which is used to save information and objects associated with that control.  TO save a link just set the controls "tag. when the control is created or later and then reference it.  Unfortunately this property cannot be dynamically set via the main data source but can contain a parallel array indexed by the eventing controls selection index.

    If you did this in WInFOrms and not WPF all of this would be much simpler.  

    As noted above - the grid should be defined in the XAML and rows added by assigning the DataSource binding to an object array,  This would then auto-generate the columns and allow the layout and style to be set in the XAML>

    WPF is very difficult for experience C/C# programmers.  With out any professional programming experience WPF will be a beast.  WinFOrms are much easier to learn and build while learning PowerShell.

    There are no good books on WPF or WinFOrms with PowerShell.

    PowerGUI and Sapinf PowerShell studio can help you build forms quickly in a form designer similar to Visual Studio.

    Here is some good information and examples for using WinForms. https://info.sapien.com/index.php/guis


    \_(ツ)_/

    Wednesday, September 11, 2019 7:17 PM
    Moderator
  • Here is the correct way to update the account expiration:

    # here $user is the user object
    Set-AdUser $user -AccountExpirationDate ([datetime]::Today.AddDays(30))
    
    #OR
    
    Get-AdUser $textbox1.Text | 
    Set-AdUser -AccountExpirationDate ([datetime]::Today.AddDays(30))
    
    #OR
    Set-AdUser $textbox1.Text -AccountExpirationDate ([datetime]::Today.AddDays(30))

    If you have a complex function that generates a date then the function needs to return a date object and accept any date setting arguments.

    function GetExpiryDate{
        Param(
            [int]$Days = 30,
            [datetime]$StartDate   
        )
        # calculate new date
        $StartDate.AddDays($Days)
    }
    
    # call it like this in form
    Set-AdUser $textbox1.Text -AccountExpirationDate (GetExpiryDate -Days 60 -StartDate [datetime]::Today)
    
    # test function as PS1 file or at a prompt
    Set-AdUser domain\userid -AccountExpirationDate (GetExpiryDate -Days 60 -StartDate [datetime]::Today)

    Note that the code runs outside of a form allowing us to easily test and debug our function.  Functions in forms should never be passed controls or form objects.  Only what can be tested at a prompt should be in a function.

    Again - learning PowerSHell correctly would teach you most of this including how to design and debug PS code.


    \_(ツ)_/




    • Edited by jrvModerator Wednesday, September 11, 2019 7:30 PM
    Wednesday, September 11, 2019 7:28 PM
    Moderator
  • How to correctly create a ComboBox:

    #~~< ComboBox1 >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    $ComboBox1 = New-Object System.Windows.Forms.ComboBox
    $ComboBox1.FormattingEnabled = $true
    $ComboBox1.Location = '480, 26'
    $ComboBox1.Size = '234, 24'
    $ComboBox1.TabIndex = 34
    $items = 'Abu Dhabi', 'Amsterdam', 'Bangkok', 'Barcelona', 'Beijing', 'Brussels', 'Bucharest', 'Casablanca', 'Delhi', 'Dubai', 'Düsseldorf', 'Frankfurt', 'Hong Kong', 'Istanbul', 'London', 'Luxembourg', 'Madrid', 'Milan', 'Moscow', 'München', 'New York', 'Paris', 'Perth', 'Prague', 'Rome', 'Sao Paulo', 'Seoul', 'Shanghai', 'Singapore', 'Sydney', 'Tokyo', 'Warsaw', 'Washington'
    $ComboBox1.Items.AddRange($items)
    # move following line to form "Load" event. $ComboBox1.SelectedIndex = -1

    Use a label for "Select Office" so it doesn't end up in the list.

    With PowerShell we do not need to use the "Drawing" primitives as PS knows how to convert the strings for point and location.


    \_(ツ)_/


    • Edited by jrvModerator Wednesday, September 11, 2019 7:36 PM
    Wednesday, September 11, 2019 7:35 PM
    Moderator
  • WPF documentation has one very  big issue with the examples.  The examples do not use event arguments correctly.

    This is an example:

    void ChangeForeground(object sender, RoutedEventArgs e)
    {
        if (btn1.Foreground == Brushes.Green){
            btn1.Foreground = Brushes.Black;
            btn1.Content = "Foreground";
        }else{
            btn1.Foreground = Brushes.Green;
            btn1.Content = "Control foreground(text) changes from black to green.";
        }
    }

    The code was likely written by old VB programmers with minimal experience in WPF or C#.

    This is what is would look like corrected:

    void ChangeForeground(Button btn, RoutedEventArgs e)
    {
        if(btn.Foreground == Brushes.Green){
            btn.Foreground = Brushes.Black;
            btn.Content = "Foreground";
        }else{
            btn.Foreground = Brushes.Green;
            btn.Content = "Control foreground(text) changes from black to green.";
        }
    }

    This makes the event code control independent allowing the same code to be used on any button and hides the reference in the call preventing collisions.

    In PS this is how it converts:

    $button_ChangeForeground = {
        # $this is "sender" object and "$_" is "EventArgsRouted" object
        if($this.Foreground -eq 'Green'){
            $this.Foreground = 'Black'
            $this.Content = 'Foreground'
        }else{
            $this.Foreground = 'Green'
            $this.Content = "Control foreground(text) changes from black to green.";
        }
    }

    Note the similarity.

    Also events are additive.  We can add multiple events of the same type to a control as long as the events are totally independent of each other.

    $button1.add_MouseDoubleClick($button_ChangeForeground)
    $button1.add_MouseDoubleClick($button_DoOtherTask)


    \_(ツ)_/


    • Edited by jrvModerator Wednesday, September 11, 2019 7:54 PM
    Wednesday, September 11, 2019 7:54 PM
    Moderator
  • All true.

    $this is the only variable that is received by the doubleclick due to scope.

    I am working through the grid selection. I have experience with this so it shouldn't take long.

    The scoping will require some work and may just work out better to place a hidden column with the full concatenated url.

    I have marked this answered

    Thank you

    Not true at all.  All events send the arguments.  The MouseDoubleCLick event send "EventArgsRouted" as "$_" but it contains no useful information other than the "Cancel" property shared by all events. Other events contain things like graphics context. Row and Column index and other useful information depending on the event and the control.  You have to either look it up in the docs or cast it to the event type.

    Example: https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.datagrid.beginningedit?view=netframework-4.8


    \_(ツ)_/

    Wednesday, September 11, 2019 8:31 PM
    Moderator