I'm hoping this is the correct place for this, I'm struggling a little for wording as i'm not sure what you would call this.
Basically, i have a system in place where my datagrid marks cells that have changed with a new background colour, to do this i have a method in the object that contains these properties that receives a string which is the name of the property to check, and then a switch statement that takes that string to check the correct property.
public Color HasChanged(string value)
{
switch (value)
{
case "CILRef":
if (_localShipment.cilRef != _originalShipment.cilRef)
{
return Colors.SkyBlue;
}
else
{
return Colors.White;
}
case "ArrivedAtPortDate":
if (_localShipment.arrivedAtPortDate != _originalShipment.arrivedAtPortDate)
{
return Colors.SkyBlue;
}
else
{
return Colors.White;
}
}
I've removed the rest of the properties for brevity.
Now i get the nagging sensation that there is a cleaner way to do this string>property without using a switch statement, but i can't for the life of me find anything on google, it's hard to search without some keyword to go on.
I'm also attempting to only save those properties that have changed, i was going to place any changed property name into an array, and then have a loop with yet another switch statement that checked that array and then saved the correct property. However this again seems untidy to me.
is there a cleaner solution to this, hopefully that could handle the addition of new properties without needing to add new code to the switch statements.
I can include the rest of the code that does this checking (namely the WPF binding on the datagrid, and a converter that calls the checking method with the property name as a string parameter) if needed.
EDIT:
To show the rest of my code, hopefully explaining a few things.
I have a datagrid in xaml that contains these properties. below is an example:
<DataGridTemplateColumn Header="CILRef">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding CILRef}">
<TextBlock.Background>
<SolidColorBrush Color="{Binding Converter={StaticResource hasChangedConverter}, ConverterParameter='CILRef'}"/>
</TextBlock.Background>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding CILRef, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
as you can see, by this line : <SolidColorBrush Color="{Binding Converter={StaticResource hasChangedConverter}, ConverterParameter='CILRef'}"/>
The background is bound using a converter, which is:
class HasChangedConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
try
{
var shipment = value as Shipment;
var property = parameter as string;
return shipment.HasChanged(property);
}
catch (Exception ex)
{
return Colors.HotPink;
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new Exception("The method or operation is not implemented.");
}
}
Hopefully this will explain how the code works better than i've been doing in the comments.
3 Answers 3
Ok, so having spoken to others on stackoverflow, i have change the code, this (i hope) satisfies all the issue's I've pointed out and all the issues pointed out to me.
The HasChanged is now:
public Color HasChanged(string value)
{
try
{
var data1 = _localShipment.GetType().GetProperty(value, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).GetValue(_localShipment, null);
var data2 = _originalShipment.GetType().GetProperty(value, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).GetValue(_originalShipment, null);
return data1 != data2 ? Colors.SkyBlue : Colors.White;
}
catch (Exception ex)
{
return Colors.White;
}
}
EDIT:
This has been changed to the following methods:
private object GetPropValue(object src, string propName)
{
PropertyInfo p = src.GetType().GetProperty(propName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
string value = (string)p.GetValue(src, null);
return value;
}
public bool HasChanged(string value)
{
var data1 = GetPropValue(_localShipment, value);
var data2 = GetPropValue(_originalShipment, value);
return data1 != data2 ? true : false;
}
#endregion
}
and the converter has been changed to :
class HasChangedConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null) { return Colors.White; }
var shipment = value as Shipment;
var property = parameter as string;
if (shipment.HasChanged(property)) { return Colors.SkyBlue; } else { return Colors.White; }
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new Exception("The method or operation is not implemented.");
}
}
This solves the issues mentioned previously in this question.
-
1\$\begingroup\$ This is basically what I was going to post haha. It looks good except for in your
catch
block you should instead handle the exception. (Unless of course you just want to consume the exception and return a default color...) \$\endgroup\$Max– Max2014年03月05日 13:01:11 +00:00Commented Mar 5, 2014 at 13:01 -
\$\begingroup\$ @Max: The part of your comment in parentheses is wrong, but only because the type of the exception is
Exception
. If there's aStackOverflowException
or anOutOfMemoryException
, trying to handle it is a very bad idea. No one wants that. This is more for the questioner's benefit than yours, though, since I assume you already know that. \$\endgroup\$Magus– Magus2014年03月05日 16:04:05 +00:00Commented Mar 5, 2014 at 16:04 -
\$\begingroup\$ See the edits above, this has been changed in an attempt to solve the issues people have pointed out. \$\endgroup\$Ben– Ben2014年03月05日 16:33:18 +00:00Commented Mar 5, 2014 at 16:33
- As you already discovered, if you want to access a member based on a string name, use reflection.
- You shouldn't mix business logic with presentation:
HasChanged
shouldn't return aColor
, it should return abool
. The colors should the be defined separately. - Use
as
only when you expect to get the wrong type and in that case, always test fornull
. If you're not going to do that, just use a cast. - I don't think that indicating a general exception using a color is a good idea, because it hides all information about what specific exception occurred. If you're expecting only some specific exception, then catch only that.
-
\$\begingroup\$ Thanks for this. Much clearer than trying to understand via comments. I've already changed the gas change to return a boil and the converter handles the color. The try catch has been removed. Quick question tho. I've always been under the assumption that using as was faster and cleaner than a cast. That comes from uni and is most likely wrong \$\endgroup\$Ben– Ben2014年03月05日 13:54:03 +00:00Commented Mar 5, 2014 at 13:54
-
\$\begingroup\$ Using as and a null check is going to be faster than cast and catch for NullReferenceException, that's probably where the speed argument comes from. But if you're not going to do that, just use a cast. And I think that clearer code is the one that more accurately describes your intentions. And here, that's a cast, not as. \$\endgroup\$svick– svick2014年03月05日 14:50:34 +00:00Commented Mar 5, 2014 at 14:50
-
\$\begingroup\$ Ok, great reply, sorry about the big typos in the original comment, i was attempting to reply via phone for the first time. and it clearly did not go well. \$\endgroup\$Ben– Ben2014年03月05日 14:52:32 +00:00Commented Mar 5, 2014 at 14:52
Well it seems to me you have a key and a chunk of code that returns a colour based on some boolean check.
I would probably suggest a Function collection.
e.g
Dictionary<string,Func<Color>> Actions;
void InitActions()
{
Actions = new Dictionary<string,Func<Color>>();
Actions.add("CILRef",()=>
{
return _localShipment.cilRef != _originalShipment.cilRef ? Colors.SkyBlue : Color.White;
});
}
and to use it:
public Color HasChanged(string value)
{
Color highlightColor = Color.White;
if(Actions.Contains(value))
highlightColor = Actions[value];
return highlightColor;
}
Although that doesn't really solve your problem.
I usually like to answer a persons question first in case they choose not to do a large refactor but I would take a closer look at what you are trying to do.
You have a number of larger design problems:
- You have a lot of repetition.
- Next nested loops/logic are usually good indications of requiring a refactor.
- You have "Magic Strings", what if one of those is misspelt, will your whole application break?
- You shouldn't be tying application logic directly to the ui in the first place.
Ideally I imagine you want something more akin to:
void DataGridSelectionChanged(object sender,EventArgs e)
{
var grid = sender as Grid;
if(grid == null) return;
string selectedValue = grid.SelectedValue ?? DefaultValue;
Color highlightColor = GetHighlightColour(selectedValue);
...
}
In short the prospect of setting the highlight colour should not really be dependent on the raw values, and if indeed it has to be it should be done in a safe manner. as it is there are a number of way's to break your application some accidentally while developing others while using the application by passing invalid data.
-
\$\begingroup\$ I appreciate your comments, This system was requested by the designer, it highlights changes to the datagrid that will be saved in the next "save all", these changes must be data based (different from the loaded data). Also the default on the switch case throws an ArgumentOutOfRangeException. I hope this clears up some of your concerns, and explains why I've chosen to do it this way. \$\endgroup\$Ben– Ben2014年03月05日 10:40:05 +00:00Commented Mar 5, 2014 at 10:40
-
\$\begingroup\$ @user1412240 one should not use Exceptions for "expected" errors. Actually I do that myself more often than I want to admit, but you should instead return a meaningful errorvalue. ;)# \$\endgroup\$Vogel612– Vogel6122014年03月05日 10:52:42 +00:00Commented Mar 5, 2014 at 10:52
-
\$\begingroup\$ Just to note, the string value that is passed is hard coded, not a parameter that is set by the user. It should only ever error if i've made a mistake during development, never during runtime. \$\endgroup\$Ben– Ben2014年03月05日 11:06:39 +00:00Commented Mar 5, 2014 at 11:06
-
1\$\begingroup\$ @user1412240 which goes back to my original point, why use raw strings that could cause a mistake in the first place? If they are hard coded and never change replace them for const strings and refer to them instead. \$\endgroup\$apieceoffruit– apieceoffruit2014年03月05日 11:08:18 +00:00Commented Mar 5, 2014 at 11:08
-
\$\begingroup\$ I think it would help if i provided the rest of the code, perhaps that will explain better than i can via comments. Please see my edit (in a few mins) \$\endgroup\$Ben– Ben2014年03月05日 11:09:14 +00:00Commented Mar 5, 2014 at 11:09
onChange="this.style.add('background-color', '#lightblueRGB');"
\$\endgroup\$