Rappels et validation des propriétés de dépendance (WPF .NET)
Cet article explique comment définir une propriété de dépendance et implémenter des rappels de propriété de dépendance. Les rappels prennent en charge la validation de valeur, la contrainte de valeur et d’autres logiques nécessaires lorsqu’une valeur de propriété change.
Prérequis
L’article suppose une connaissance de base des propriétés de dépendance et que vous avez lu la vue d’ensemble des propriétés de dépendance. Pour suivre les exemples de cet article, il vous aide à connaître le langage XAML (Extensible Application Markup Language) et à savoir comment écrire des applications WPF.
Rappels de validation de valeur
Les rappels validate-value permettent de vérifier si une nouvelle valeur de propriété de dépendance est valide avant d’être appliquée par le système de propriétés. Ce rappel déclenche une exception si la valeur ne répond pas aux critères de validation.
Les rappels validate-valeur ne peuvent être attribués qu’à une propriété de dépendance une seule fois pendant l’inscription de propriété. Lors de l’inscription d’une propriété de dépendance, vous avez la possibilité de transmettre une ValidateValueCallback référence à la Register(String, Type, Type, PropertyMetadata, ValidateValueCallback) méthode. Les rappels validate-valeur ne font pas partie des métadonnées de propriété et ne peuvent pas être substitués.
La valeur effective d’une propriété de dépendance est sa valeur appliquée. La valeur effective est déterminée par la précédence de la valeur de propriété lorsque plusieurs entrées basées sur des propriétés existent. Si un rappel de valeur de validation est inscrit pour une propriété de dépendance, le système de propriétés appelle son rappel de valeur de validation lors de la modification de la valeur, en passant la nouvelle valeur en tant qu’objet. Dans le rappel, vous pouvez renvoyer l’objet valeur au type inscrit auprès du système de propriétés, puis exécuter votre logique de validation sur celle-ci. Le rappel retourne true
si la valeur est valide pour la propriété, sinon false
.
Si un rappel validate-valeur retourne false
, une exception est levée et la nouvelle valeur n’est pas appliquée. Les développeurs d’application doivent être prêts à gérer ces exceptions. Une utilisation courante des rappels validate-valeur consiste à valider des valeurs d’énumération ou à limiter les valeurs numériques lorsqu’elles représentent des mesures qui ont des limites. Les rappels de valeur de validation sont appelés par le système de propriétés dans différents scénarios, notamment :
- Initialisation d’objet, qui applique une valeur par défaut au moment de la création.
- Appels programmatiques vers SetValue.
- Les métadonnées remplacent qui spécifient une nouvelle valeur par défaut.
Les rappels validate-value n’ont pas de paramètre qui spécifie l’instance DependencyObject sur laquelle la nouvelle valeur est définie. Toutes les instances d’un DependencyObject
partage sont les mêmes rappels de valeur de validation. Il ne peut donc pas être utilisé pour valider des scénarios spécifiques à l’instance. Pour plus d’informations, consultez ValidateValueCallback.
L’exemple suivant montre comment empêcher une propriété, typée comme Doubledéfinie PositiveInfinity ou NegativeInfinity.
public class Gauge1 : Control
{
public Gauge1() : base() { }
// Register a dependency property with the specified property name,
// property type, owner type, property metadata, and callbacks.
public static readonly DependencyProperty CurrentReadingProperty =
DependencyProperty.Register(
name: "CurrentReading",
propertyType: typeof(double),
ownerType: typeof(Gauge1),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: double.NaN,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure),
validateValueCallback: new ValidateValueCallback(IsValidReading));
// CLR wrapper with get/set accessors.
public double CurrentReading
{
get => (double)GetValue(CurrentReadingProperty);
set => SetValue(CurrentReadingProperty, value);
}
// Validate-value callback.
public static bool IsValidReading(object value)
{
double val = (double)value;
return !val.Equals(double.NegativeInfinity) &&
!val.Equals(double.PositiveInfinity);
}
}
Public Class Gauge1
Inherits Control
Public Sub New()
MyBase.New()
End Sub
Public Shared ReadOnly CurrentReadingProperty As DependencyProperty =
DependencyProperty.Register(
name:="CurrentReading",
propertyType:=GetType(Double),
ownerType:=GetType(Gauge1),
typeMetadata:=New FrameworkPropertyMetadata(
defaultValue:=Double.NaN,
flags:=FrameworkPropertyMetadataOptions.AffectsMeasure),
validateValueCallback:=New ValidateValueCallback(AddressOf IsValidReading))
Public Property CurrentReading As Double
Get
Return GetValue(CurrentReadingProperty)
End Get
Set(value As Double)
SetValue(CurrentReadingProperty, value)
End Set
End Property
Public Shared Function IsValidReading(value As Object) As Boolean
Dim val As Double = value
Return Not val.Equals(Double.NegativeInfinity) AndAlso
Not val.Equals(Double.PositiveInfinity)
End Function
End Class
public static void TestValidationBehavior()
{
Gauge1 gauge = new();
Debug.WriteLine($"Test value validation scenario:");
// Set allowed value.
gauge.CurrentReading = 5;
Debug.WriteLine($"Current reading: {gauge.CurrentReading}");
try
{
// Set disallowed value.
gauge.CurrentReading = double.PositiveInfinity;
}
catch (ArgumentException e)
{
Debug.WriteLine($"Exception thrown by ValidateValueCallback: {e.Message}");
}
Debug.WriteLine($"Current reading: {gauge.CurrentReading}");
// Current reading: 5
// Exception thrown by ValidateValueCallback: '∞' is not a valid value for property 'CurrentReading'.
// Current reading: 5
}
Public Shared Sub TestValidationBehavior()
Dim gauge As New Gauge1()
Debug.WriteLine($"Test value validation scenario:")
' Set allowed value.
gauge.CurrentReading = 5
Debug.WriteLine($"Current reading: {gauge.CurrentReading}")
Try
' Set disallowed value.
gauge.CurrentReading = Double.PositiveInfinity
Catch e As ArgumentException
Debug.WriteLine($"Exception thrown by ValidateValueCallback: {e.Message}")
End Try
Debug.WriteLine($"Current reading: {gauge.CurrentReading}")
' Current reading: 5
' Exception thrown by ValidateValueCallback: '∞' is not a valid value for property 'CurrentReading'.
' Current reading 5
End Sub
Rappels modifiés par propriété
Les rappels modifiés par propriété vous informent lorsque la valeur effective d’une propriété de dépendance a changé.
Les rappels modifiés par propriété font partie des métadonnées de propriété de dépendance. Si vous dérivez d’une classe qui définit une propriété de dépendance ou ajoutez votre classe en tant que propriétaire d’une propriété de dépendance, vous pouvez remplacer les métadonnées. En cas de substitution de métadonnées, vous avez la possibilité de fournir une nouvelle PropertyChangedCallback référence. Utilisez un rappel modifié par propriété pour exécuter une logique nécessaire lorsqu’une valeur de propriété change.
Contrairement aux rappels validate-value, les rappels modifiés par les propriétés ont un paramètre qui spécifie l’instance DependencyObject sur laquelle la nouvelle valeur est définie. L’exemple suivant montre comment un rappel modifié par propriété peut utiliser la DependencyObject
référence d’instance pour déclencher des rappels de valeur de force.
Rappels de valeur de force
Les rappels de valeur coerce vous permettent d’être averti lorsque la valeur effective d’une propriété de dépendance est sur le point de changer, afin que vous puissiez ajuster la nouvelle valeur avant son application. En plus d’être déclenché par le système de propriétés, vous pouvez appeler des rappels de valeur de force à partir de votre code.
Les rappels de valeur de coerce font partie des métadonnées de propriété de dépendance. Si vous dérivez d’une classe qui définit une propriété de dépendance ou ajoutez votre classe en tant que propriétaire d’une propriété de dépendance, vous pouvez remplacer les métadonnées. Lorsque vous substituez les métadonnées, vous avez la possibilité de fournir une référence à un nouveau CoerceValueCallback. Utilisez un rappel de valeur de force pour évaluer de nouvelles valeurs et les forcer si nécessaire. Le rappel retourne la valeur coerced si la contrainte s’est produite ; sinon elle retourne la nouvelle valeur non définie.
Comme pour les rappels modifiés par les propriétés, les rappels de valeur de contrainte ont un paramètre qui spécifie l’instance DependencyObject sur laquelle la nouvelle valeur est définie. L’exemple suivant montre comment un rappel de valeur de coerce peut utiliser une référence d’instance DependencyObject
pour forcer les valeurs de propriété.
Remarque
Les valeurs de propriété par défaut ne peuvent pas être coédées. Une propriété de dépendance a sa valeur par défaut définie sur l’initialisation d’objet ou lorsque vous effacez d’autres valeurs à l’aide ClearValuede .
Rappels de co-valeur et de propriété modifiés en combinaison
Vous pouvez créer des dépendances entre les propriétés d’un élément à l’aide de rappels de valeur de contrainte et de rappels modifiés par propriété en combinaison. Par exemple, les modifications apportées à une contrainte de propriété ou à une nouvelle évaluation dans une autre propriété de dépendance. L’exemple suivant montre un scénario courant : trois propriétés de dépendance qui stockent respectivement la valeur actuelle, la valeur minimale et la valeur maximale d’un élément d’interface utilisateur. Si la valeur maximale change afin qu’elle soit inférieure à la valeur actuelle, la valeur actuelle est alors définie sur la nouvelle valeur maximale. Et, si la valeur minimale change afin qu’elle soit supérieure à la valeur actuelle, la valeur actuelle est ensuite définie sur la nouvelle valeur minimale. Dans l’exemple, la PropertyChangedCallback valeur actuelle appelle explicitement les CoerceValueCallback valeurs minimales et maximales.
public class Gauge2 : Control
{
public Gauge2() : base() { }
// Register a dependency property with the specified property name,
// property type, owner type, property metadata, and callbacks.
public static readonly DependencyProperty CurrentReadingProperty =
DependencyProperty.Register(
name: "CurrentReading",
propertyType: typeof(double),
ownerType: typeof(Gauge2),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: double.NaN,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback: new PropertyChangedCallback(OnCurrentReadingChanged),
coerceValueCallback: new CoerceValueCallback(CoerceCurrentReading)
),
validateValueCallback: new ValidateValueCallback(IsValidReading)
);
// CLR wrapper with get/set accessors.
public double CurrentReading
{
get => (double)GetValue(CurrentReadingProperty);
set => SetValue(CurrentReadingProperty, value);
}
// Validate-value callback.
public static bool IsValidReading(object value)
{
double val = (double)value;
return !val.Equals(double.NegativeInfinity) && !val.Equals(double.PositiveInfinity);
}
// Property-changed callback.
private static void OnCurrentReadingChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
depObj.CoerceValue(MinReadingProperty);
depObj.CoerceValue(MaxReadingProperty);
}
// Coerce-value callback.
private static object CoerceCurrentReading(DependencyObject depObj, object value)
{
Gauge2 gauge = (Gauge2)depObj;
double currentVal = (double)value;
currentVal = currentVal < gauge.MinReading ? gauge.MinReading : currentVal;
currentVal = currentVal > gauge.MaxReading ? gauge.MaxReading : currentVal;
return currentVal;
}
// Register a dependency property with the specified property name,
// property type, owner type, property metadata, and callbacks.
public static readonly DependencyProperty MaxReadingProperty = DependencyProperty.Register(
name: "MaxReading",
propertyType: typeof(double),
ownerType: typeof(Gauge2),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: double.NaN,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback: new PropertyChangedCallback(OnMaxReadingChanged),
coerceValueCallback: new CoerceValueCallback(CoerceMaxReading)
),
validateValueCallback: new ValidateValueCallback(IsValidReading)
);
// CLR wrapper with get/set accessors.
public double MaxReading
{
get => (double)GetValue(MaxReadingProperty);
set => SetValue(MaxReadingProperty, value);
}
// Property-changed callback.
private static void OnMaxReadingChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
depObj.CoerceValue(MinReadingProperty);
depObj.CoerceValue(CurrentReadingProperty);
}
// Coerce-value callback.
private static object CoerceMaxReading(DependencyObject depObj, object value)
{
Gauge2 gauge = (Gauge2)depObj;
double maxVal = (double)value;
return maxVal < gauge.MinReading ? gauge.MinReading : maxVal;
}
// Register a dependency property with the specified property name,
// property type, owner type, property metadata, and callbacks.
public static readonly DependencyProperty MinReadingProperty = DependencyProperty.Register(
name: "MinReading",
propertyType: typeof(double),
ownerType: typeof(Gauge2),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: double.NaN,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback: new PropertyChangedCallback(OnMinReadingChanged),
coerceValueCallback: new CoerceValueCallback(CoerceMinReading)
),
validateValueCallback: new ValidateValueCallback(IsValidReading));
// CLR wrapper with get/set accessors.
public double MinReading
{
get => (double)GetValue(MinReadingProperty);
set => SetValue(MinReadingProperty, value);
}
// Property-changed callback.
private static void OnMinReadingChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
depObj.CoerceValue(MaxReadingProperty);
depObj.CoerceValue(CurrentReadingProperty);
}
// Coerce-value callback.
private static object CoerceMinReading(DependencyObject depObj, object value)
{
Gauge2 gauge = (Gauge2)depObj;
double minVal = (double)value;
return minVal > gauge.MaxReading ? gauge.MaxReading : minVal;
}
}
Public Class Gauge2
Inherits Control
Public Sub New()
MyBase.New()
End Sub
' Register a dependency property with the specified property name,
' property type, owner type, property metadata, And callbacks.
Public Shared ReadOnly CurrentReadingProperty As DependencyProperty =
DependencyProperty.Register(
name:="CurrentReading",
propertyType:=GetType(Double),
ownerType:=GetType(Gauge2),
typeMetadata:=New FrameworkPropertyMetadata(
defaultValue:=Double.NaN,
flags:=FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnCurrentReadingChanged),
coerceValueCallback:=New CoerceValueCallback(AddressOf CoerceCurrentReading)),
validateValueCallback:=New ValidateValueCallback(AddressOf IsValidReading))
' CLR wrapper with get/set accessors.
Public Property CurrentReading As Double
Get
Return GetValue(CurrentReadingProperty)
End Get
Set(value As Double)
SetValue(CurrentReadingProperty, value)
End Set
End Property
' Validate-value callback.
Public Shared Function IsValidReading(value As Object) As Boolean
Dim val As Double = value
Return Not val.Equals(Double.NegativeInfinity) AndAlso Not val.Equals(Double.PositiveInfinity)
End Function
' Property-changed callback.
Private Shared Sub OnCurrentReadingChanged(depObj As DependencyObject, e As DependencyPropertyChangedEventArgs)
depObj.CoerceValue(MinReadingProperty)
depObj.CoerceValue(MaxReadingProperty)
End Sub
' Coerce-value callback.
Private Shared Function CoerceCurrentReading(depObj As DependencyObject, value As Object) As Object
Dim gauge As Gauge2 = CType(depObj, Gauge2)
Dim currentVal As Double = value
currentVal = If(currentVal < gauge.MinReading, gauge.MinReading, currentVal)
currentVal = If(currentVal > gauge.MaxReading, gauge.MaxReading, currentVal)
Return currentVal
End Function
Public Shared ReadOnly MaxReadingProperty As DependencyProperty =
DependencyProperty.Register(
name:="MaxReading",
propertyType:=GetType(Double),
ownerType:=GetType(Gauge2),
typeMetadata:=New FrameworkPropertyMetadata(
defaultValue:=Double.NaN,
flags:=FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnMaxReadingChanged),
coerceValueCallback:=New CoerceValueCallback(AddressOf CoerceMaxReading)),
validateValueCallback:=New ValidateValueCallback(AddressOf IsValidReading))
' CLR wrapper with get/set accessors.
Public Property MaxReading As Double
Get
Return GetValue(MaxReadingProperty)
End Get
Set(value As Double)
SetValue(MaxReadingProperty, value)
End Set
End Property
' Property-changed callback.
Private Shared Sub OnMaxReadingChanged(depObj As DependencyObject, e As DependencyPropertyChangedEventArgs)
depObj.CoerceValue(MinReadingProperty)
depObj.CoerceValue(CurrentReadingProperty)
End Sub
' Coerce-value callback.
Private Shared Function CoerceMaxReading(depObj As DependencyObject, value As Object) As Object
Dim gauge As Gauge2 = CType(depObj, Gauge2)
Dim maxVal As Double = value
Return If(maxVal < gauge.MinReading, gauge.MinReading, maxVal)
End Function
' Register a dependency property with the specified property name,
' property type, owner type, property metadata, And callbacks.
Public Shared ReadOnly MinReadingProperty As DependencyProperty =
DependencyProperty.Register(
name:="MinReading",
propertyType:=GetType(Double),
ownerType:=GetType(Gauge2),
typeMetadata:=New FrameworkPropertyMetadata(
defaultValue:=Double.NaN,
flags:=FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnMinReadingChanged),
coerceValueCallback:=New CoerceValueCallback(AddressOf CoerceMinReading)),
validateValueCallback:=New ValidateValueCallback(AddressOf IsValidReading))
' CLR wrapper with get/set accessors.
Public Property MinReading As Double
Get
Return GetValue(MinReadingProperty)
End Get
Set(value As Double)
SetValue(MinReadingProperty, value)
End Set
End Property
' Property-changed callback.
Private Shared Sub OnMinReadingChanged(depObj As DependencyObject, e As DependencyPropertyChangedEventArgs)
depObj.CoerceValue(MaxReadingProperty)
depObj.CoerceValue(CurrentReadingProperty)
End Sub
' Coerce-value callback.
Private Shared Function CoerceMinReading(depObj As DependencyObject, value As Object) As Object
Dim gauge As Gauge2 = CType(depObj, Gauge2)
Dim minVal As Double = value
Return If(minVal > gauge.MaxReading, gauge.MaxReading, minVal)
End Function
End Class
Scénarios de rappel avancés
Contraintes et valeurs souhaitées
Si une valeur définie localement d’une propriété de dépendance est modifiée par contrainte, la valeur définie localement inchangée est conservée comme valeur souhaitée. Si le forçage est basé sur d’autres valeurs de propriété, le système de propriétés réévalue dynamiquement le forçage chaque fois que ces autres valeurs changent. Dans les contraintes du forçage, le système de propriétés applique une valeur la plus proche de la valeur souhaitée. Si la condition de contrainte ne s’applique plus, le système de propriétés restaure la valeur souhaitée, en supposant qu’aucune valeur de précédence plus élevée n’est active. L’exemple suivant teste la contrainte dans la valeur actuelle, la valeur minimale et le scénario de valeur maximale.
public static void TestCoercionBehavior()
{
Gauge2 gauge = new()
{
// Set initial values.
MinReading = 0,
MaxReading = 10,
CurrentReading = 5
};
Debug.WriteLine($"Test current/min/max values scenario:");
// Current reading is not coerced.
Debug.WriteLine($"Current reading: " +
$"{gauge.CurrentReading} (min: {gauge.MinReading}, max: {gauge.MaxReading})");
// Current reading is coerced to max value.
gauge.MaxReading = 3;
Debug.WriteLine($"Current reading: " +
$"{gauge.CurrentReading} (min: {gauge.MinReading}, max: {gauge.MaxReading})");
// Current reading is coerced, but tracking back to the desired value.
gauge.MaxReading = 4;
Debug.WriteLine($"Current reading: " +
$"{gauge.CurrentReading} (min: {gauge.MinReading}, max: {gauge.MaxReading})");
// Current reading reverts to the desired value.
gauge.MaxReading = 10;
Debug.WriteLine($"Current reading: " +
$"{gauge.CurrentReading} (min: {gauge.MinReading}, max: {gauge.MaxReading})");
// Current reading remains at the desired value.
gauge.MinReading = 5;
gauge.MaxReading = 5;
Debug.WriteLine($"Current reading: " +
$"{gauge.CurrentReading} (min: {gauge.MinReading}, max: {gauge.MaxReading})");
// Current reading: 5 (min=0, max=10)
// Current reading: 3 (min=0, max=3)
// Current reading: 4 (min=0, max=4)
// Current reading: 5 (min=0, max=10)
// Current reading: 5 (min=5, max=5)
}
Public Shared Sub TestCoercionBehavior()
' Set initial values.
Dim gauge As New Gauge2 With {
.MinReading = 0,
.MaxReading = 10,
.CurrentReading = 5
}
Debug.WriteLine($"Test current/min/max values scenario:")
' Current reading is not coerced.
Debug.WriteLine($"Current reading: " &
$"{gauge.CurrentReading} (min={gauge.MinReading}, max={gauge.MaxReading})")
' Current reading is coerced to max value.
gauge.MaxReading = 3
Debug.WriteLine($"Current reading: " &
$"{gauge.CurrentReading} (min={gauge.MinReading}, max={gauge.MaxReading})")
' Current reading is coerced, but tracking back to the desired value.
gauge.MaxReading = 4
Debug.WriteLine($"Current reading: " &
$"{gauge.CurrentReading} (min={gauge.MinReading}, max={gauge.MaxReading})")
' Current reading reverts to the desired value.
gauge.MaxReading = 10
Debug.WriteLine($"Current reading: " &
$"{gauge.CurrentReading} (min={gauge.MinReading}, max={gauge.MaxReading})")
' Current reading remains at the desired value.
gauge.MinReading = 5
gauge.MaxReading = 5
Debug.WriteLine($"Current reading: " &
$"{gauge.CurrentReading} (min={gauge.MinReading}, max={gauge.MaxReading})")
' Current reading: 5 (min=0, max=10)
' Current reading: 3 (min=0, max=3)
' Current reading: 4 (min=0, max=4)
' Current reading: 5 (min=0, max=10)
' Current reading: 5 (min=5, max=5)
End Sub
Des scénarios de dépendance assez complexes peuvent se produire lorsque vous avez plusieurs propriétés dépendantes les unes des autres de manière circulaire. Techniquement, il n’existe aucun problème avec les dépendances complexes, sauf qu’un grand nombre de réévaluations peuvent réduire les performances. En outre, les dépendances complexes exposées dans l’interface utilisateur peuvent confondre les utilisateurs. Traitez PropertyChangedCallback et CoerceValueCallback aussi sans ambiguïté que possible, et ne limitez pas trop.
Annuler les modifications de valeur
En retournant UnsetValue à partir d’un CoerceValueCallback, vous pouvez rejeter une modification de valeur de propriété. Ce mécanisme est utile lorsqu’une modification de valeur de propriété est lancée de manière asynchrone, mais lorsqu’elle est appliquée n’est plus valide pour l’état actuel de l’objet. Un autre scénario peut être de supprimer sélectivement une modification de valeur en fonction de son origine. Dans l’exemple suivant, l’appel CoerceValueCallback
de la GetValueSource méthode, qui retourne une ValueSource structure avec une BaseValueSource énumération qui identifie la source de la nouvelle valeur.
// Coerce-value callback.
private static object CoerceCurrentReading(DependencyObject depObj, object value)
{
// Get value source.
ValueSource valueSource =
DependencyPropertyHelper.GetValueSource(depObj, CurrentReadingProperty);
// Reject any property value change that's a locally set value.
return valueSource.BaseValueSource == BaseValueSource.Local ?
DependencyProperty.UnsetValue : value;
}
' Coerce-value callback.
Private Shared Function CoerceCurrentReading(depObj As DependencyObject, value As Object) As Object
' Get value source.
Dim valueSource As ValueSource =
DependencyPropertyHelper.GetValueSource(depObj, CurrentReadingProperty)
' Reject any property value that's a locally set value.
Return If(valueSource.BaseValueSource = BaseValueSource.Local, DependencyProperty.UnsetValue, value)
End Function
Voir aussi
.NET Desktop feedback