Вот мой собственный ответ:
Вместо того, чтобы создавать подклассы TextEdit, что кажется слишком сложным, я просто перехватываю события KeyPress и скрываю каждый нажатый символ до события EditValueChanging. Я храню уникальные токены в фальшивой строке, чтобы я мог выяснить, что нужно изменить в моей SecureString при каждом событии EditValueChanging.
Вот подтверждение концепции - простая форма с элементом управления TextEdit и двумя обработчиками событий:
public partial class PasswordForm : DevExpress.XtraEditors.XtraForm
{
private Random random = new Random();
private HashSet<char> pool = new HashSet<char>();
private char secret;
private char token;
private List<char> fake = new List<char>();
public PasswordForm()
{
InitializeComponent();
this.Password = new SecureString();
for (int i = 0; i < 128; i++)
{
this.pool.Add((char)(' ' + i));
}
}
public SecureString Password { get; private set; }
private void textEditPassword_EditValueChanging(object sender, DevExpress.XtraEditors.Controls.ChangingEventArgs e)
{
string value = e.NewValue as string;
// If any characters have been deleted...
foreach (char c in this.fake.ToArray())
{
if (value.IndexOf(c) == -1)
{
this.Password.RemoveAt(this.fake.IndexOf(c));
this.fake.Remove(c);
this.pool.Add(c);
}
}
// If a character is being added...
if (this.token != '\0')
{
int i = value.IndexOf(this.token);
this.Password.InsertAt(i, this.secret);
this.secret = '\0';
fake.Insert(i, this.token);
}
}
private void textEditPassword_KeyPress(object sender, KeyPressEventArgs e)
{
if (Char.IsControl(e.KeyChar))
{
this.token = '\0';
}
else
{
this.token = this.pool.ElementAt(random.Next(this.pool.Count)); // throws ArgumentOutOfRangeException when pool is empty
this.pool.Remove(this.token);
this.secret = e.KeyChar;
e.KeyChar = this.token;
}
}
}