none
Strange behavior entering text in a cell of a DataGrid (decimal point ignored)

    Question

  • This should be a face-palm kind of a question, but I've spent enough trying to wrap my brain around it. In short: when text is entered in a cell of a DataGrid (any cell), the decimal point (".", dot) is ignored (no visible response to the key press) if it follows a digit but not any other character or is the first symbol to be typed. The field ultimately expects a floating-point number and has validation, which I'll outline below, but I can't see how that can affect the decimal point. On top of that, when I designed this component years ago, I'm pretty sure that wasn't an issue (I couldn't have omitted to test non-integers), but now that I've picked it up years later, it is not working anymore. Here are the details:

    The XAML of the DataGrid:

        <DataGrid Name="DG_ProfileData" Grid.Row="3" CanUserReorderColumns="False" ColumnWidth="*" MinRowHeight="20" AutoGenerateColumns="True" CanUserResizeRows="False" AutoGeneratingColumn="OnProfDataAutoGenCol" RowEditEnding="OnPDRowEditEnding" PreviewKeyDown="OnPDPreviewKeyDown" SelectionChanged="OnProfDataSelectionChanged">
          <DataGrid.RowValidationRules>
            <local:ProfileDataValidationRule ValidationStep="UpdatedValue"/>
          </DataGrid.RowValidationRules>
          <DataGrid.RowValidationErrorTemplate>
            <ControlTemplate>
              <Grid ToolTip="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridRow}}, Path=(Validation.Errors)[0].ErrorContent}">
                <Ellipse StrokeThickness="0" Fill="Red" Width="{TemplateBinding FontSize}" Height="{TemplateBinding FontSize}"/>
                <TextBlock Text="!" FontSize="{TemplateBinding FontSize}" FontWeight="Bold" Foreground="White" HorizontalAlignment="Center"/>
              </Grid>
            </ControlTemplate>
          </DataGrid.RowValidationErrorTemplate>
        </DataGrid>

    The various event handlers (some, probably not all relevant):

        private void OnProfDataAutoGenCol(object sender, DataGridAutoGeneratingColumnEventArgs e)
        {
          ((e.Column as DataGridTextColumn).Binding as Binding).UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;  // Set it so that the user-entry will be updated in e.Row.Item when editing the row
          string HeaderName = e.Column.Header.ToString();
          if(HeaderName == "Time")
            e.Column.Header = "Relative time";
          if(HeaderName == "ChannelVal")
            e.Column.Header = "Channel value";
          // In the case of "all-channel data"
          Guid CFID;  // The ID of the control function, which is used as column name
          if(Guid.TryParse(HeaderName, out CFID) && RefChannelsToSet.ContainsKey(CFID)) // Map the ID to the actual channel name
            e.Column.Header = RefChannelsToSet[CFID].PublicName;
        }
    
        private void OnPDRowEditEnding(object sender, DataGridRowEditEndingEventArgs e) // Editing a row in the DataGrid
        {
          if(e.EditAction != DataGridEditAction.Commit) // Only process the commit action
            return;
          double Time, ChanValue;
          DataGrid DG = (sender as DataGrid);
          if(CurrentCtrlFunc != AllChannelsID)  // Process individual observable collections
          {
            ProfileChanParms PCP = e.Row.Item as ProfileChanParms;
            Time = PCP.Time;
            ChanValue = PCP.ChannelVal;
            // Check for negative time values
            if(Time < 0)
            {
              MessageBox.Show("Negative times are not allowed!", "Validation error", MessageBoxButton.OK, MessageBoxImage.Error);
              int idx = ((DataGridRow)DG.ItemContainerGenerator.ContainerFromIndex(DG.SelectedIndex)).GetIndex(); // Record the original index of the edited row
              e.Cancel = true;
              DG.CancelEdit(DataGridEditingUnit.Row);
              if(idx < CopyOrgOutputControl.ControlFunctionDefinitions[CurrentCtrlFunc].TimeValueTable.Length) // As long as it is not the last row
                PCP.Time = GetTimeAtIndex(idx, CurrentCtrlFunc);  // Restore the old time value
              UpdateProfDataView(CurrentCtrlFunc);  // Re-sort to put it back into its place
              return; // Do not continue with validation
            }
            // Ensure that the zero time is present
            bool ZeroTimePresent = false;
            foreach(KeyValuePair<double, double> PCParmsKVP in GetSortedProfileData(CurrentCtrlFunc))  // Sort the data for faster processing
            {
              if(PCParmsKVP.Key == 0) // Zero time found
              {
                ZeroTimePresent = true; // Condition is satisfied
                break;
              }
            }
            if(!ZeroTimePresent)
            {
              MessageBox.Show("The profile must include values at time '0'!", "Validation error", MessageBoxButton.OK, MessageBoxImage.Error);
              e.Cancel = true;
              DG.CancelEdit();
              PCP.Time = 0; // Reset to zero
              return; // Do not continue with validation
            }
            // Check for duplicates
            foreach(ProfileChanParms PCParms in CachedPCPCol)
            {
              if(Time==PCParms.Time && ProfileData[CurrentCtrlFunc].IndexOf(PCP)!=CachedPCPCol.IndexOf(PCParms))  // New value matches an existing old value
              {
                MessageBox.Show("There may not be duplicate time values!", "Validation error", MessageBoxButton.OK, MessageBoxImage.Error);
                int idx = ((DataGridRow)DG.ItemContainerGenerator.ContainerFromIndex(DG.SelectedIndex)).GetIndex(); // Record the original index of the edited row
                if(idx < CopyOrgOutputControl.ControlFunctionDefinitions[CurrentCtrlFunc].TimeValueTable.Length) // As long as it is not the last row
                  PCP.Time = GetTimeAtIndex(idx, CurrentCtrlFunc);  // Restore the old time value
                e.Cancel = true;
                DG.CancelEdit();
                return; // Do not continue with validation
              }
            }
          }
          else  // Search the all-channels table ------------------------------------------------
          {
            Time = (double)(e.Row.Item as DataRowView)["Time"];
            // Check for negative time values
            if(Time < 0)
            {
              MessageBox.Show("Negative times are not allowed!", "Validation error", MessageBoxButton.OK, MessageBoxImage.Error);
              e.Cancel = true;
              DG.CancelEdit();
              AllChanTbl.DefaultView.Sort = "Time DESC";  // To cause an update restoring the old row
              AllChanTbl.DefaultView.Sort = "Time ASC";
              return; // Do not continue with validation
            }
            // Ensure that the zero time is present
            bool ZeroTimePresent = false;
            foreach(DataColumn DC in AllChanTbl.Columns)
            {
              Guid ChID;
              if(Guid.TryParse(DC.ColumnName, out ChID))
              {
                foreach(DataRow DR in AllChanTbl.Rows)
                {
                  if(DR[DC] != DBNull.Value)
                  {
                    if((double)DR["Time"] == 0)
                      ZeroTimePresent = true; // Condition is satisfied
                    break;
                  }
                }
              }
            }
            if(!ZeroTimePresent)
            {
              MessageBox.Show("The profile must include values at time '0'!", "Validation error", MessageBoxButton.OK, MessageBoxImage.Error);
              e.Cancel = true;
              DG.CancelEdit();
              AllChanTbl.DefaultView.Sort = "Time DESC";  // To cause an update restoring the old row
              AllChanTbl.DefaultView.Sort = "Time ASC";
              return; // Do not continue with validation
            }
            // Check for duplicates
            foreach(DataRow DR in AllChanTbl.Rows)
            {
              if(Time == (double)DR["Time"])
              {
                MessageBox.Show("There may not be duplicate time values!", "Validation error", MessageBoxButton.OK, MessageBoxImage.Error);
                e.Cancel = true;
                DG.CancelEdit();
                AllChanTbl.DefaultView.Sort = "Time DESC";  // To cause an update restoring the old row
                AllChanTbl.DefaultView.Sort = "Time ASC";
                return; // Do not continue with validation
              }
            }
          }
          SetCFParms(CurrentCtrlFunc);  // All validations have passed, commit values to the OC
        }
    
        private void OnPDPreviewKeyDown(object sender, KeyEventArgs e)
        {
          DataGrid DG = sender as DataGrid;
          if(e.Key==Key.Delete && e.IsDown) // Deleting a row in the DataGrid
          {
            if(CurrentCtrlFunc != AllChannelsID)  // Process individual observable collections
            {
              if(DG.SelectedItems.Count > 1)  // Multiple selection (no editing)
              {
                foreach(object PCPO in DG.SelectedItems)
                {
                  ProfileChanParms PCP;
                  if(PCPO is ProfileChanParms)
                    PCP = (ProfileChanParms)PCPO;
                  else
                    PCP = null;
                  if(PCP!=null && PCP.Time==0) // Not new row and incdes time 0
                  {
                    MessageBox.Show("The profile must include values at time '0'!", "Validation error", MessageBoxButton.OK, MessageBoxImage.Error);
                    e.Handled = true; // Stop propagation to prevent row deletion
                    return; // Do not continue with validation
                  }
                }
              }
              else  // Single selection, possibly editing
              {
                if(!((DataGridRow)DG.ItemContainerGenerator.ContainerFromItem(DG.SelectedItem)).IsEditing && DG.SelectedItem is ProfileChanParms && (DG.SelectedItem as ProfileChanParms).Time==0)  // Attempting to delete the row with "0" time
                {
                  MessageBox.Show("The profile must include values at time '0'!", "Validation error", MessageBoxButton.OK, MessageBoxImage.Error);
                  e.Handled = true; // Stop propagation to prevent row deletion
                  return; // Do not continue with validation
                }
              }
            }
            else
            {
              DataRowView DRV = DG.CurrentItem as DataRowView;
              if(!DRV.IsEdit && (double)DRV["Time"]==0) // Attempting to delete the row with "0" time
              {
                MessageBox.Show("The profile must include values at time '0'!", "Validation error", MessageBoxButton.OK, MessageBoxImage.Error);
                e.Handled = true; // Stop propagation to prevent row deletion
                return; // Do not continue with validation
              }
            }
          }
        }
    
        private void OnProfDataSelectionChanged(object sender, SelectionChangedEventArgs e) // Every time there is a selection change in the datagrid
        {
          if(e.AddedItems.Count>0 && CurrentCtrlFunc!=AllChannelsID)  // Whenever there is a valid selection (as in about to edit the row) and we are in single-channel mode
            CachedPCPCol = new ObservableCollection<ProfileChanParms>(ProfileData[CurrentCtrlFunc]); // Make a copy of the original collection (for tests and comparisons) as editing will modify it
        }


    The validation rule:

      public class ProfileDataValidationRule : ValidationRule // Independent validation of data (no access to Profile data)
      {
        public override ValidationResult Validate(object Value, CultureInfo CultureInfo)
        {
          if((Value as BindingGroup).Items.Count == 0)
            return ValidationResult.ValidResult;
          ConfigureProfileRE.ProfileChanParms ChParms = (Value as BindingGroup).Items[0] as ConfigureProfileRE.ProfileChanParms;
          if(ChParms!=null && ChParms.Time<0)  // <-- Expand criteria!!!!
          {
            return new ValidationResult(false, "Time may not be negative!");
          }
          else
          {
            return ValidationResult.ValidResult;
          }
        }
      }

    Maybe the type of the binding object:

        public class ProfileChanParms : INotifyPropertyChanged
        {
          private double TimeVar; // The time of this profile point
          public double Time
          {
            get { return TimeVar; }
            set
            {
              if(value != TimeVar)
              {
                TimeVar = value;
                NotifyPropertyChanged("Time");
              }
            }
          }
    
          private double ChannelValVar; // The channel value at this profile point
          public double ChannelVal
          {
            get { return ChannelValVar; }
            set
            {
              if(value != ChannelValVar)
              {
                ChannelValVar = value;
                NotifyPropertyChanged("ChannelVal");
              }
            }
          }
    
          // INotifyPropertyChanged implementation
          public event PropertyChangedEventHandler PropertyChanged; // The required "PropertyChanged" event handler
          private void NotifyPropertyChanged(string Property)
          {
            if(PropertyChanged != null)
            {
              PropertyChanged(this, new PropertyChangedEventArgs(Property));
            }
          }
        }

    And the binding of the DataGrid:

    ProfileData = new Dictionary<Guid, ObservableCollection<ProfileChanParms>>();
    
    /* ..... */
    
    DG_ProfileData.ItemsSource = ProfileData[CFID];
    

    I really don't see what it could be but it seems like something is either expecting an integer or general text, which makes the validation kick in, but that is the only time it lets me type the decimal point.

    I'd appreciate some constructive assistance!

    Kamen


    Currently using Visual Studio 2017, native C++; (Windows API) and C# (.Net, WPF), on Windows 10 Pro 64-bit; Mountain Time zone.




    • Edited by Kamen Friday, June 8, 2018 8:57 PM
    Friday, June 8, 2018 8:32 PM

Answers

  • You want to put the following line :

    FrameworkCompatibilityPreferences.KeepTextBoxDisplaySynchronizedWithTextProperty = False;

    in the constructor your class Application. This was a very confusing thing to many people and I have no idea why Microsoft changed this.


    Lloyd Sheen

    • Marked as answer by Kamen Friday, June 8, 2018 9:26 PM
    Friday, June 8, 2018 9:20 PM

All replies

  • You want to put the following line :

    FrameworkCompatibilityPreferences.KeepTextBoxDisplaySynchronizedWithTextProperty = False;

    in the constructor your class Application. This was a very confusing thing to many people and I have no idea why Microsoft changed this.


    Lloyd Sheen

    • Marked as answer by Kamen Friday, June 8, 2018 9:26 PM
    Friday, June 8, 2018 9:20 PM
  • Thank you! I put this statement in the constructor of the main window and now it behaves properly.

    This is quite crazy, for MS to do such major code-breaking change! I'll need to research this and see what else it may have affected. Any good places to start?

    Kamen


    Currently using Visual Studio 2017, native C++; (Windows API) and C# (.Net, WPF), on Windows 10 Pro 64-bit; Mountain Time zone.


    • Edited by Kamen Friday, June 8, 2018 9:31 PM
    Friday, June 8, 2018 9:28 PM
  • Should have added to be careful using this as it always has it may change in future releases of dot.net and like this change I'm sure we won't be notified.

    https://docs.microsoft.com/en-ca/dotnet/api/system.windows.frameworkcompatibilitypreferences.keeptextboxdisplaysynchronizedwithtextproperty?view=netframework-4.7.1#System_Windows_FrameworkCompatibilityPreferences_KeepTextBoxDisplaySynchronizedWithTextProperty


    Lloyd Sheen


    • Edited by sqlguy Friday, June 8, 2018 9:47 PM
    Friday, June 8, 2018 9:45 PM
  • Thanks, again!

    Scary, scary stuff...

    Kamen


    Currently using Visual Studio 2017, native C++; (Windows API) and C# (.Net, WPF), on Windows 10 Pro 64-bit; Mountain Time zone.


    • Edited by Kamen Friday, June 8, 2018 10:38 PM
    Friday, June 8, 2018 9:47 PM
  • This is exactly the problem I have too. I am new to WPF. You say "put this statement in the constructor of the main window." How/where do I do that? Thanks.
    Tuesday, November 27, 2018 3:53 AM
  • I worked it out - I put the line in Sub New of the project with the issue.

    Thanks for making this information available - I was completely stuck.

    Tuesday, November 27, 2018 3:59 AM
  • I worked it out - I put the line in Sub New of the project with the issue.

    Thanks for making this information available - I was completely stuck.

    I'm glad you did - I was just typing a response! :-)

    Kamen


    Currently using Visual Studio 2013 U5, native C&#43;&#43; (Windows API) and C# (.Net, WPF), on Windows 7 64-bit; Mountain Time zone.

    Tuesday, November 27, 2018 4:00 AM