To aid in the localization of .resx files, I made a generic resx comparer that allows you to edit the values of the resx's. Also, this is my first proper adventure in WPF (I've just been using Windows-runtime before).
This is my view interface:
public interface IResxTranslationHelperWindow
{
object DataContext { get; set; }
event EventHandler<DataGridCellEditEndingEventArgs> EndCellEdit;
event EventHandler<ResxValues> DeleteRow;
void Show();
}
The view itself:
<Grid Name="MainGrid">
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<converters:WindowWidthToHalf x:Key="WindowWidthToHalfConverter" />
</StackPanel.Resources>
<Button Content="{x:Static properties:Resources.FilePickerButton_BaseResx}" Command="{Binding PickFile}" CommandParameter="DefaultResx"
Width="{Binding ElementName=MainGrid, Path=ActualWidth, Converter={StaticResource WindowWidthToHalfConverter}}" />
<Button Content="{x:Static properties:Resources.FilePickerButton_LocalizedResx}" Command="{Binding PickFile}"
Width="{Binding ElementName=MainGrid, Path=ActualWidth, Converter={StaticResource WindowWidthToHalfConverter}}" />
</StackPanel>
<DataGrid ItemsSource="{Binding ResxStrings}" LoadingRow="GridDisplay_LoadingRow" CanUserSortColumns="True"
CanUserAddRows="True" CanUserDeleteRows="True" ColumnWidth="300" AutoGenerateColumns="False" BeginningEdit="DataGrid_BeginningEdit"
CellEditEnding="GridDisplay_CellEditEnding" Grid.Row="1" PreviewKeyDown="DataGrid_PreviewKeyDown">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=Key}" Header="{x:Static properties:Resources.DataGridHeader_Key_Header}" CanUserSort="True" />
<DataGridTextColumn Binding="{Binding Path=Value}" Header="{x:Static properties:Resources.DataGridHeader_Value_Header}" CanUserSort="True" />
<DataGridTextColumn Binding="{Binding Path=LocalizedValue}" Header="{x:Static properties:Resources.DataGridHeader_LocalizedValue_Header}" CanUserSort="True" />
</DataGrid.Columns>
</DataGrid>
</Grid>
And the code-behind:
public ResxTranslationHelperWindow()
{
InitializeComponent();
}
private void GridDisplay_LoadingRow(object sender, DataGridRowEventArgs e)
{
var item = e.Row.Item as ResxValues;
if (item == null)
{
e.Row.Background = new SolidColorBrush(Colors.White);
return;
}
if (string.IsNullOrEmpty(item.LocalizedValue))
{
e.Row.Background = new SolidColorBrush(Colors.LightSeaGreen);
}
if (string.IsNullOrEmpty(item.Value))
{
e.Row.Background = new SolidColorBrush(Colors.Red);
}
if (!string.IsNullOrEmpty(item.Value) && !string.IsNullOrEmpty(item.LocalizedValue))
{
e.Row.Background = new SolidColorBrush(Colors.White);
}
}
private void DataGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
// keys must not be edited unless they are empty, which signifies adding a new value
if (e.Column.DisplayIndex == 0)
{
var originalText = ((TextBlock) e.EditingEventArgs.OriginalSource).Text;
if (originalText != "")
{
e.Cancel = true;
}
}
}
private void GridDisplay_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
if (e.EditAction == DataGridEditAction.Commit)
{
try
{
OnEndCellEdit(e);
}
catch (ArgumentException)
{
var grid = (DataGrid)sender;
var itemsLastRemoved = grid.ItemsSource.OfType<ResxValues>()
.ToList();
itemsLastRemoved.RemoveAt(itemsLastRemoved.Count - 1);
grid.ItemsSource = new ObservableCollection<ResxValues>(itemsLastRemoved);
grid.SelectedItem = itemsLastRemoved.Last();
}
}
}
private void DataGrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (Key.Delete == e.Key)
{
var grid = sender as DataGrid;
OnRowDeleted(grid.SelectedItem as ResxValues);
}
}
public event EventHandler<DataGridCellEditEndingEventArgs> EndCellEdit;
protected virtual void OnEndCellEdit(DataGridCellEditEndingEventArgs e)
{
var handler = EndCellEdit;
if (handler != null)
{
handler(this, e);
}
}
public event EventHandler<ResxValues> DeleteRow;
protected virtual void OnRowDeleted(ResxValues e)
{
var handler = DeleteRow;
if (handler != null)
{
handler(this, e);
}
}
My VM:
private readonly IResxTranslationHelperWindow _window;
private XElement _data;
private XElement _localizedData;
public ObservableCollection<ResxValues> ResxStrings { get; set; }
public string DataPath { get; set; }
public string LocalizedDataPath { get; set; }
private ICommand _pickFile;
public ICommand PickFile
{
get
{
return _pickFile ?? (_pickFile = new RelayCommand
(
param =>
{
var filePath = OpenFilePicker();
if (filePath == "") { return; }
if (param as string == "DefaultResx")
{
DataPath = filePath;
}
else
{
LocalizedDataPath = filePath;
}
LoadData();
}
));
}
}
public ResxTranslationHelperVM(IResxTranslationHelperWindow window)
{
_window = window;
ResxStrings = new ObservableCollection<ResxValues>();
_window.EndCellEdit += EndCellEdit;
_window.DeleteRow += DeleteRow;
window.DataContext = this;
}
private string OpenFilePicker()
{
var filePicker = new OpenFileDialog
{
Multiselect = false,
SupportMultiDottedExtensions = true,
CheckPathExists = true,
Filter = @"Resx Files|*.resx"
};
filePicker.ShowDialog();
return filePicker.FileName;
}
private void EndCellEdit(object sender, DataGridCellEditEndingEventArgs e)
{
var key = e.Column.DisplayIndex == 0
? ((TextBox)e.EditingElement).Text
: ResxStrings[e.Row.GetIndex()].Key;
var value = e.Column.DisplayIndex == 1
? ((TextBox)e.EditingElement).Text
: ResxStrings[e.Row.GetIndex()].Value;
var localizedValue = e.Column.DisplayIndex == 2
? ((TextBox)e.EditingElement).Text
: ResxStrings[e.Row.GetIndex()].LocalizedValue;
if (e.Row.Item as ResxValues == ResxStrings.Last() &&
ResxStrings.Select(s => s.Key).Contains(key))
{
MessageBox.Show(Resources.EndCellEdit_KeyAlreadyExists, Resources.EndCellEdit_AddValueError, MessageBoxButtons.OK, MessageBoxIcon.Warning);
throw new ArgumentException("Invalid data");
}
RemoveNodes(ResxStrings[e.Row.GetIndex()].Key);
AddNode(_data, key, value);
AddNode(_localizedData, key, localizedValue);
SaveFiles();
}
private void DeleteRow(object sender, ResxValues e)
{
RemoveNodes(e.Key);
SaveFiles();
ResxStrings.Remove(e);
}
private void SaveFiles()
{
_data.Save(DataPath);
_localizedData.Save(LocalizedDataPath);
}
private void RemoveNodes(string key)
{
var node = _localizedData.Nodes().OfType<XElement>().FirstOrDefault(n => n.FirstAttribute.Value == key);
if (node != null) { node.Remove(); }
node = _data.Nodes().OfType<XElement>().FirstOrDefault(n => n.FirstAttribute.Value == key);
if (node != null) { node.Remove(); }
}
private void AddNode(XElement element, string key, string value)
{
if (key == null) { return; }
XNamespace ns = "http://www.w3.org/XML/1998/namespace";
var newValue = new XElement("data");
newValue.SetAttributeValue("name", key);
newValue.SetAttributeValue(ns + "space", "preserve");
newValue.SetElementValue("value", value);
element.Add(newValue);
}
public void Load()
{
_window.Show();
}
private void LoadData()
{
if (string.IsNullOrEmpty(DataPath) || string.IsNullOrEmpty(LocalizedDataPath))
{
return;
}
ResxStrings.Clear();
_data = XElement.Load(DataPath);
var dataStrings = _data.Nodes().OfType<XElement>().Where(n => n.LastAttribute.Name.LocalName == "space"
&& n.LastAttribute.Value == "preserve"
&& n.FirstAttribute.Name.LocalName == "name");
_localizedData = XElement.Load(LocalizedDataPath);
var localizedDataStrings = _localizedData.Nodes().OfType<XElement>().Where(n => n.LastAttribute.Name.LocalName == "space"
&& n.LastAttribute.Value == "preserve"
&& n.FirstAttribute.Name.LocalName == "name").ToList();
// add all strings in data
foreach (var node in dataStrings)
{
var localizedValue = localizedDataStrings.FirstOrDefault(n => n.FirstAttribute.Value == node.FirstAttribute.Value);
ResxStrings.Add(new ResxValues(node.FirstAttribute.Value, node.Value.Trim(), localizedValue == null ? null : localizedValue.Value.Trim()));
}
// add all strings in localized data not already added
foreach (var node in localizedDataStrings.Where(n => !ResxStrings.Select(s => s.Key).Contains(n.FirstAttribute.Value)))
{
ResxStrings.Add(new ResxValues(node.FirstAttribute.Value, null, node.Value.Trim()));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
The App startup file:
protected override void OnStartup(StartupEventArgs e)
{
var translationHelperVM = new ResxTranslationHelperVM(new ResxTranslationHelperWindow());
translationHelperVM.Load();
}
The ResxValues
class:
public string Key { get; set; }
public string Value { get; set; }
public string LocalizedValue { get; set; }
public ResxValues(string key, string value, string localizedValue)
{
Key = key;
Value = value;
LocalizedValue = localizedValue;
}
public ResxValues() : this("", "", "") { }
And the RelayCommand
class:
public event EventHandler CanExecuteChanged;
private readonly Action<object> _execute;
public RelayCommand(Action<object> execute)
{
_execute = execute;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_execute(parameter);
}
You can view (and use) the whole project on GitHub, and of course all comments are welcome.