.resxファイルのローカライズを支援するために、resxの値を編集できる汎用のresx比較子を作成しました。また、これはWPFでの私の最初の適切な冒険です(私は以前にWindowsランタイムを使用したことがあります)。
これは私のビューインターフェイスです:
public interface IResxTranslationHelperWindow
{
object DataContext { get; set; }
event EventHandler EndCellEdit;
event EventHandler DeleteRow;
void Show();
}
ビュー自体:
そしてコードビハインド:
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()
.ToList();
itemsLastRemoved.RemoveAt(itemsLastRemoved.Count - 1);
grid.ItemsSource = new ObservableCollection(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 EndCellEdit;
protected virtual void OnEndCellEdit(DataGridCellEditEndingEventArgs e)
{
var handler = EndCellEdit;
if (handler != null)
{
handler(this, e);
}
}
public event EventHandler DeleteRow;
protected virtual void OnRowDeleted(ResxValues e)
{
var handler = DeleteRow;
if (handler != null)
{
handler(this, e);
}
}
私のVM:
private readonly IResxTranslationHelperWindow _window;
private XElement _data;
private XElement _localizedData;
public ObservableCollection 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();
_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().FirstOrDefault(n => n.FirstAttribute.Value == key);
if (node != null) { node.Remove(); }
node = _data.Nodes().OfType().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().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().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));
}
}
アプリの起動ファイル:
protected override void OnStartup(StartupEventArgs e)
{
var translationHelperVM = new ResxTranslationHelperVM(new ResxTranslationHelperWindow());
translationHelperVM.Load();
}
ResxValues
クラス
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("", "", "") { }
RelayCommand
クラス
public event EventHandler CanExecuteChanged;
private readonly Action
プロジェクト全体を GitHubで表示(および使用)できます。もちろん、すべてのコメントを歓迎します。
私はあなたのコードのWPF部分について少し考えを加えたいと思います。
If you need your buttons to occupy half the screen, you should simply replace StackPanel
with Grid
:
This is not the case where you should use a converter.
GridDisplay_LoadingRow
- now this is a different story. Here converter will fit nicely. You can data bind your data grid item to Background
property, and use a converter to set the appropriate Brush
depending on item properties. You can use regular converter, or you can use a multibinding. The latter wil update background dynamically, if you implement INotifyPropertyChanged
interface on your ResxValues
class.
DataGrid_BeginningEdit
- and what if user added a new item but noticed a typo afterwards? Why can't he edit it? He can remove the entire row and re-add it though. Thats a really weird design from UX standpoint. :) If you want to keep this logic, I would suggest using xaml and data binding instead. You can use DataGridTemplateColumn
with custom CellEditingTemplate
which will be a TextBox
for editable cells or TextBlock
for read only cells. You will select one or the other by binding to your data grid items. This might be a bit complicated if you've just started learning wpf though.
GridDisplay_CellEditEnding
- so, if user makes a mistake you remove the entire row? :) That's not very user-friendly. I am not sure I understand, why you are recreating ItemsSource
either, seems fishy to me (will it update the collection on your viewmodel? or will it break the binding?). WPF have an inbuild system for validating data errors. Normally you would want to let user know, that he made an error (show tooltip, highlight with red border, etc.) and give him a chance to fix it.
DataGrid_PreviewKeyDown
- you should use InputBindings
instead.
上記の提案に従うことを選択した場合、ファイルの裏にあるコードはリファクタリング後に次のようになります。
public ResxTranslationHelperWindow()
{
InitializeComponent();
}
GridDisplay_LoadingRow()
メソッドでは、現在の行の Background
を設定した後に戻る必要があります。それ以外の場合、たとえば item.LocalizedValue
と item.Value
がどちらも == string.Empty
の場合、ちらつきが発生する可能性があります。
Be consistent. One time you are checking string.IsNullOrEmpty()
, one time you use != ""
. If you are sure that a string isn't null
you might just check variable.Length > 0
or != string.Empty
. A ""
check will lead to you rechecking is there really no space between (if you eyes getting worse).
DataGrid_BeginningEdit()
メソッドには、constに抽出されるべきマジックナンバー 0
があります。さらに、guard句を使用して DisplayIndex
に必要な値( 0
)が含まれていない場合に返すことで、より明確になります。
GridDisplay_CellEditEnding()
メソッドで itemsLastRemoved
が空になる可能性がある場合は、 Count
プロパティに関するチェックを追加する必要があります。そうでなければ、 catch
は負のインデックスで RemoveAt()
を呼び出すことによってスローされます。
繰り返しになりますが、if(e.EditAction!= DataGridEditAction.Commit){return;}というガード句を使用してください。
ResxTranslationHelperVM
クラスでは、パブリックにアクセス可能である必要がないプロパティセッター private
を作成することを検討する必要があります。
EndCellEdit()
メソッドにはより多くのマジックナンバーがあります。これも
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"); }
私のお気に入りのコントロールフローではありません。特定のケースで GridDisplay_CellEditEnding()
メソッドで処理されるように Argument
をスローしています。方法。
The condition ResxStrings.Select(s => s.Key).Contains(key)
will first iterate over ResxStrings
and select the Key
property and that by calling Contains()
on the result you are iterating over the "keys" until you find a matching one.
A more obvious and performant way would be to use the Any()
method like ResxStrings.Any(s => s.Key == key)
.
RemoveNodes()
メソッドは、1つのノード
のみを削除するため、少し誤解を招く可能性があります。そのため、メソッドに RemoveNode()
という名前を付けたほうが良いでしょう。
node!= null
の場合はここでチェックしていますが、 node.parent!= null
の場合は確認を忘れています。ノード上で Remove()
を呼び出すことによって node.parent == null
と InvalidOperationException
がスローされる場合があります。
In the LoadData()
method you have some duplicated code which could be extracted to a separate method. In addition one time you are using the result as IEnumerable
and the other time you are calling ToList()
on the IEnumerable
.
これを抽出することをお勧めします
private const string xLastLocalName = "space";
private const string xValue = "preserve";
private const string xFirstLocalName = "name";
private IEnumerable LoadElements(string filePath)
{
var element = XElement.Load(filePath);
return element.Nodes().OfType().Where(n => n.LastAttribute.Name.LocalName == xLastLocalName
&& n.LastAttribute.Value == xValue
&& n.FirstAttribute.Name.LocalName == xFirstLocalName );
}
const
の名前で私は本当に満足していません。
これにより、 LoadData()
メソッドの呼び出しが以下のようになります。
var dataStrings = LoadElements(DataPath);
var localizedDataStrings = LoadElements(LocalizedDataPath);
ファイルが存在しない可能性がある場合は、この場合も処理する必要があります。
// 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())); }
それはひどい水平スクロールです。 Linqをループの外側に移動して改行を追加することをお勧めします。
var matching = localizedDataStrings.Where(
n => !ResxStrings.Select(s => s.Key)
.Contains(n.FirstAttribute.Value)
);
// add all strings in localized data not already added
foreach (var node in matching)
{
ResxStrings.Add(new ResxValues(node.FirstAttribute.Value, null, node.Value.Trim()));
}
もちろん、この時点で、あなたは全体のkittenkaboodleをlinq-ifyingすることを考慮したいかもしれません。
これにはバグがあります。
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())); }
まず、明白な Trim()
があります。それは故意に接頭辞/接尾辞があるスペースを削除します。その理由は私が間違った値を得ていたことです。このような値になると、 \ n {value} \ n \ n {comment} \ n
の形式になります。値(およびコメント)を取得する正しい方法は次のとおりです。
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.Nodes().OfType().FirstOrDefault(n => n.Name.LocalName == "value").Value));
}
コメントの値を取得するには、 "value"
を "comment"
に置き換えます。