Порядок инициализации управления Fiasco
Рассмотрим следующий код:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<Slider ValueChanged="slider_ValueChanged/>
<TextBox x:Name="counter"/>
</StackPanel>
</Window>
а также
namespace Project1
{
public partial class Window1 : Window
{
public MainWindow() { InitializeComponent(); }
void slider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
counter.Text = e.NewValue.ToString();
}
}
}
Ползунок поднимет ее ValueChanged
событие во время инициализации пока counter
все еще null
,
Это пример более крупной проблемы, с которой я столкнулся при использовании WPF, что события пользовательского интерфейса могут запускаться в любое время, и что нет единого места, где я мог бы поместить свой код инициализации, чтобы он гарантированно выполнялся после всех указатели, принадлежащие системе WPF, были инициализированы, но до того, как сработали какие-либо события пользовательского интерфейса.
Какой самый элегантный способ справиться с этим? Тот факт, что этот конкретный пример должен использовать привязку данных, не имеет значения.
2 ответа
Истинный ответ на этот вопрос заключается в использовании шаблона MVVM, в котором код окна за файлами практически не содержит кода инициализации.
В этом шаблоне пользовательский интерфейс связан с остальной частью кода только с привязкой данных. Вы пишете специальные классы модели представления, которые реализуют INotifyPropertyChanged
и возьмите свою бизнес-логику и представьте ее как ряд свойств, к которым привязан пользовательский интерфейс.
Естественно, вы полностью контролируете, как инициализируются ваши view-модели.
Есть много способов справиться с этим, в зависимости от вашей ситуации
Во-первых, вы можете просто распознать тот факт, что объект не может быть инициализирован, и проверить это перед обработкой. Например,
if (counter.Text != null)
counter.Text = e.NewValue.ToString();
Во-вторых, вы можете прикрепить свои события к событию Loaded объекта, чтобы они не срабатывали до тех пор, пока объект не будет инициализирован.
void Counter_Loaded(object sender, EventArgs e)
{
slider.ValueChanged += Slider_ValueChanged;
}
void Counter_Unloaded(object sender, EventArgs e)
{
slider.ValueChanged -= Slider_ValueChanged;
}
И наконец, вы можете использовать диспетчер WPF для запуска событий в потоке пользовательского интерфейса в другом DispatcherPriority. По умолчанию Normal
, который бежит после Loaded
, Render
, а также DataBind
операции
Dispatcher.BeginInvoke(DispatcherPriority.DataBind,
new Action(delegate() { counter.Text = e.NewValue.ToString(); }));