Laden...

TextBox-Caret springt nach der 10. Eingabe ans Ende der Selection

Letzter Beitrag vor 10 Stunden 7 Posts 181 Views
TextBox-Caret springt nach der 10. Eingabe ans Ende der Selection

Hallo,

ich hänge gerade an einem Problem, das mir einige Rätsel aufgibt. Ich versuche es so einfach wie möglich zu beschreiben:

Ich habe eine TextBox, in die nur die Ziffern von 0-9, sowie ein ',' eingegeben werden können. Ich möchte letztendlich einen Währungsbetrag angeben können. Aus der DB wird ein in meinem ViewModel ein decimal gelesen und der wird von einem ValueConverter in der View als string ausgegeben.

Im ValueConverter rufe ich eine CurrencyConversion-Klasse (im Code untern "ThirdConverter" --> der ungünstige Name steht da, weil ich seit Tagen Fehlersuche betreibe und dies der 3. Konverter ist, mit dem ich versuche das Problem zu lösen...), die aus dem decimal einen string im gewünschten Währungsformat ausgiebt. Ich weiß, es gibt buil-in-Lösungen, aber

  1. war der Boiler-Plate-Code nicht sooo schwer und
  2. möchte ich ein ganz bestimmtes Format --> so soll etwa der decimal Betrag 12746 als "12 746,00 €" angezeigt werden. Also, Ziffern immer in 3er-Gruppen, Komma vor den Cent-Beträgen, Leerzeichen, Währungszeichen.

Meine Methoden liefern das gewünschte Resultat: ich gebe einen Betrag ein, z. B. 1425,36 und bekomme in der Textbox "1 425,36 €" angezeigt.

Wichtig ist hierbei: nach jeder Eingabe befindet sich das Caret in der Textbox immer an der richigen Stelle - da musste ich auch gar nix machen, das regelt die Textbox selbst.

PROBLEM: Zumindest dachte ich, dass die Textbox selbst regelt. Denn aus unerfindlichen Gründen springt das Caret bei der Eingabe von 1234567891 (bei Eingabe der letzten 1) ganz ans Ende der Selection. Ich kann dann zwar das Caret hinter der 1 positionieren und eine weitere Eingabe machen, ab dann wird es aber immer ganz ans Ende springen (die Formatierung wird dabei aber weiterhin tadellos in 3-er-Gruppen angezeigt).

Nachfolgend mein Code.

Zur Beachtung: er ist aus meinem Test-Projekt eingefügt, d.h. ich habe ihn so einfach wie nur irgend möglich gehalten. Man kann in dieser Version nur Ziffern eingeben, andere Zeichen crashen die Anwendung. Das "springende" Caret ist ja das Problem.

Meine CurrencyConverter-Klasse (heißt hier ThirdConverter --> Gründe s. oben)

 public static decimal? ConvertCurrencyStringToDecimal(string? _input)
 {     
     StringBuilder sb = new StringBuilder();
     sb.Append(_input.Replace(" ", string.Empty));
     
     return decimal.Parse(sb.ToString());
 }

 public static string? ConvertDecimalValueToCurrencyString(decimal? _input)
 {    
     var regularValueResult = FinalValueConversion(_input);

     return regularValueResult;
 }
 
 //Helfer-Methoden

 private static string? FinalValueConversion(decimal? value)
 {
     //Define string builder for output string
     StringBuilder preString = new StringBuilder();
     StringBuilder arrangedOutput = new StringBuilder();

     decimal x = Convert.ToDecimal(value);
     preString.Append(x.ToString("0.00"));
    
     if (preString.ToString().Contains(","))
     {
         arrangedOutput.Append(ArrangeOutputString(preString.ToString()));
     }
     
     return arrangedOutput.ToString();
 }

 private static string TrimOutputString(decimal? value)
 {
     if (value == null)
     { return null; }

     StringBuilder final_pre = new StringBuilder();
     StringBuilder final_post = new StringBuilder();
     StringBuilder final = new StringBuilder();

     decimal _value = Convert.ToDecimal(value); // convet decimal? to decimal...
     final.Append(_value.ToString("#.00")); //...make it a string

     var vsArray = final.ToString().Split(',');

     if (vsArray.Length > 1)
     {
         if (vsArray[1].Length > 2)
         {
             final.Clear();

             final_post.Append(vsArray[1].Remove(2));

             final_pre.Append(vsArray[0]);
             final_pre.Append(",");

             final.Append(final_post.ToString());
         }
     }

     return final.ToString();
 }

 #region Arrangement of output string methods
 private static string ArrangeOutputString(string _preFinalString)
 {
     StringBuilder sb = new StringBuilder();

     string[] dummyArray = _preFinalString.Split(',');
     string preCommaValue = dummyArray[0];
     string postCommaValue = dummyArray[1];

     var dummyList = ConsolidateDigitGroups(preCommaValue);
     string finalString = AssembleGroupsOfThree(dummyList);

     sb.Append(finalString).Append(",").Append(postCommaValue);

     return sb.ToString();
 }

 private static List<string> ConsolidateDigitGroups(string _unconsolidatedString) //neueste
 {
     StringBuilder sb = new StringBuilder();
     List<string> tmpList = new List<string>();

     int numberOfDigits = _unconsolidatedString.Count();

     while (numberOfDigits > 3)
     {
         //IMPORTANT! Add the LAST 3 digits first. E.g.: 1566 - 566 must be truncated first in order to get the final 1 566.
         tmpList.Add(_unconsolidatedString.Substring(_unconsolidatedString.Length - 3));

         //Remove the digits that were truncated from the original input.
         _unconsolidatedString = _unconsolidatedString.Remove(_unconsolidatedString.Length - 3);

         //Lower the number of remaining digits by 3 (i.e. the amount of digits that just were truncated)
         numberOfDigits -= 3;
     }

     //If after subtracting 3 each time the while-loop is true, a rest greater than zero remains, add the remaining strings to the list.
     if (numberOfDigits > 0)
     {
         tmpList.Add(_unconsolidatedString);
     }

     return tmpList;
 }

 private static string AssembleGroupsOfThree(List<string> stringChunks)
 {
     StringBuilder sb = new StringBuilder();
     //Define an output string.
     string outputString = string.Empty;

     //Define a limit for the following for-loop - it is equal to the number of entries in the list.
     int limit = stringChunks.Count;

     //Define an indexer that will access the entries in the stringChunks-List.
     //The indexer will start with the integer which represents the maximum number of entries.
     int indexer = limit;

     for (int i = 1; i <= limit; i++)
     {
         //Append the LEAST index of the stringChunks-List. [The the list is 0-based index, the actual indexer is indexer - 1]
         //With each iteration the indexer will get closer to 0. --> MUST end with zero!
         //outputString += stringChunks[indexer - 1];
         sb.Append(stringChunks[indexer - 1]);

         //Lower the indexer by 1
         indexer--;

         //Check, wheter the current iteration is less than the limit. As long as the expression is true, add a grouping placeholder (" ").
         if (i < limit)
         {
             //outputString += " ";
             sb.Append(" ");
         }
     }

     //return outputString;
     return sb.ToString();
 }
 #endregion

Hier mein Value-Converter

public class TestConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // Convert a decimal to a string
       var resultString = ThirdConverter.ConvertDecimalValueToCurrencyString((decimal)value);

        return resultString;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // Convert a currencs string to a decimal
        var result = ThirdConverter.ConvertCurrencyStringToDecimal(value.ToString());
        return result;
    }
}

Und mein xaml-code für die View

UserControl x:Class="ValidateAndConvert_Views.TestView"
            [...]
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <conversion:TestConverter x:Key="testConverter"/>
    </UserControl.Resources>
    <Grid Background="LightGray">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <!-- Content -->
        <Grid Grid.Row="1">            
            <TextBox Text="{Binding UserInput, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource testConverter}, ConverterParameter=true}">
                
            </TextBox>
        </Grid>
        
    </Grid>
</UserControl>

Lösungsversuche bisher:

Ich habe beim Debuggen Haltepunkte gesetzt (im ValueConverter): dort wird der decimal 1234567891 zu "1 234 567 891,00" konvertiert. Hier springt nun aber das Caret ans Ende. Bei allen vorherigen Ziffern - nicht.

--> Wenn ich die Formatierung weglasse (also das arrangieren in 3-er-Gruppen und das Hinzufügen der Dezimalstellen), dann kann ich natürlich so viel eingeben, wie ich möchte. Trotzdem verstehe ich nicht, wieso es nach Eingabe von 9 Zeichen abrupt nicht mehr funktioniert, zumal weiterhin korrekt formatiert ausgegeben wird, wenn ich das Caret manuell an die richtige Stelle setze.

Hilfen:

  • für mich wäre Rückmeldung hilfreich, ob mein Code bei euch zum selben Resultat führt. Ich hatte neulich sehr merkwürdige "quirks" in Visual Studio und kann nicht ausschließen, dass es damit zu tun hat.
  • kann man irgendwie beim Debuggen feststellen, ob die Textbox das Tracing zum Caret "verliert"?
  • Hat mein Code einen (offensichtlichen) Fehler?

Vorab vielen Dank!

viele Grüße

Vorph

Kannst du nicht einfach ein NumberFormatInfo dafür erstellen (und dies dann einer CultureInfo zuweisen), s.a. Standardmäßige Zahlenformatzeichenfolgen: Spezifizierer für Währungsformat (C) ?

Ansonsten schau auch mal in WpfCurrencyTextbox rein.

Danke für den Hinweis - so auf den ersten Blick vermute ich, dass ein Großteil der Funktionen, diie ich implementiert habe, dann wegfallen:

Die Eingabe von "," führt in meinem Fall z. B. dazu, dass "0,00 €" abgezeigt werden, das Caret aber an der richtigen Stelle (nach dem Komma) steht. Das macht die TextBox nach der Umwandlung von selbst.

"-", "-0", "-,", "-0,0" werden korrekt als "-0,00 €" interpretiert (mit dem Caret an der Position  nach dem Komma, bzw. nach der ersten Null.

Auch das Löschen der Eingaben funktioniert wie intendiert.

Lediglich bei 99 Miliarden 999 Millionen 999 Tausend 999 ist Schluss - wenn ich jetzt noch eine Ziffer eingebe, springt das Caret hinter das Euro-Zeichen. Sonst bleibt es immer vor dem Komma^^

Ich tendiere langsam Richtung Bug: wie bereits erwähnt - im Converter wird sowohl in der Convert-, als auch  der ConvertBack-Methode jeweils der korrekte Wert returniert - danach gibt es keine von mir selbst geschriebene Logik mehr, zumal schon gar keine, die die Position des Carets beeinflusst.

Zitat von GeneVorph

Ich tendiere langsam Richtung Bug:

In .NET? Die Wahrscheinlichkeit ist nahezu 0.
Deine verwendeten Technologien sind so dermaßen weit verbreitet, etabliert und stabil, dass quasi jeder Bug bekannt ist - wenn überhaupt hier einer existiert.

Dein Code hat aber mit Sicherheit Bugs, allein das ganze Region-Handling strikt auf Komma etc... auch das ganze StringBuilder-Handling macht so kein wirklichen sinn.

Wo genau nun Dein inhaltlicher Bug ist, sehe ich auch nicht; aber die Wahrscheinlichkeit, dass es an .NET/WPF liegt, ist echt gering.

Hallo Gene,

als erstes würde ich den Converter Code aufräumen, wie es Th69 bereits vorgeschlagen hat. Mit folgendem Snippet sollte dein gewünschtes Ergebnis herauskommen:

NumberFormatInfo info = new()
{
    CurrencySymbol = "€",
    CurrencyDecimalDigits = 2,
    CurrencyGroupSeparator = " ",
    CurrencyDecimalSeparator = ",",
    CurrencyPositivePattern = 3,
    CurrencyNegativePattern = 8
};
decimal value = 123_456_789_012_345.131516m;
string text = value.ToString("c", info); // 123 456 789 012 345,13 €

Zu dem Caret Problem:
Woher sollte die Textbox den Zusammenhang von dem eingegebenen Text und dem dahinterliegenden Converter kennen? Die verlinkte CurrencyTextBox wird mit Sicherheit eine interne Logik haben, die Eingabe und Caret Position speziell behandelt. Womöglich wirst du dies selbst machen müssen, da diese TB nur vorgegebene Formate unterstützt (laut Beschreibung).

Guten Morgen zusammen!

Ähnlich Spooks Ansatz wäre das hier mein Versuch an die Thematik ran zu gehen:

        public static string FormatCurrency(decimal input)
        {
            CultureInfo culture = new CultureInfo("de-DE");
            culture.NumberFormat.CurrencyGroupSeparator = " ";
            culture.NumberFormat.CurrencySymbol = "€";
            string output = string.Format(culture, "{0:C2}", input);
            return output;
        }

mfg s3nf
(Leben != Ponyhof)