Я создаю приложение, которое отслеживает несколько серверов трассировки на удаленных компьютерах и отображает обработанные данные в пользовательском интерфейсе. Когда я использую симулятор, генерирующий данные, все работает так, как ожидалось. Проблема в том, что пользовательский интерфейс не обновляется при использовании фактической модели данных. Иногда он обновляется один раз после запуска, иногда никогда. ViewModel нетронута, данные правильно обработаны и доступны. Это влияет на все элементы управления, использующие привязку в XAML. Другие элементы управления, которые обновляются в коде, работают должным образом.
Все ViewModels правильно реализованы INotifyPropertyChanged.
См. следующий пример:
Пользовательский контроль TimeStrip
В этом TimeStrip элементе управления текстовое поле и линия со временем должны перемещаться вправо.
XAML пользовательского управления:
<Grid x:Name = "TimeSlotGrid">
//..
<local:TimeSlot Grid.Column = "2" Text = "8:00"/>
<local:TimeSlot Grid.Column = "3" Text = "9:00"/>
//...
</Grid>
<Grid LayoutUpdated = "Grid_LayoutUpdated" Loaded = "Grid_Loaded">
<Canvas x:Name = "TimeCanvas">
<TextBlock Style = "{StaticResource MediumTextTheme}"
Text = "{Binding Path=UiStuff.CurrentTimeString}"
Canvas.Left = "{Binding Path=UiStuff.TimeTextPos}"
Canvas.Top = "1"/>
<Line x:Name = "CurrentTimeCursor"
X1 = "{Binding Path=UiStuff.TimePos}"
X2 = "{Binding Path=UiStuff.TimePos}"
Y1 = "0" Y2 = "360" Stroke = "Red" StrokeThickness = "2"/>
</Canvas>
</Grid>
Серые прямоугольники (TimeSlot) обновляются с помощью Grid_LayoutUpdated, который устанавливает градиент фона в соответствии с текущим временем:
Код позади
private void Grid_LayoutUpdated(object sender, EventArgs e)
{
MainViewModel context = DataContext as MainViewModel;
context.UiStuff.GridWidth = TimeCanvas.ActualWidth;
DateTime dt = context.UiStuff.CurrentTime;
int currentHour = dt.AddHours(-6).Hour;
double currentHourPart = dt.Minute / 60d;
for (int i = 0; i < TimeSlotGrid.Children.Count; i++)
{
TimeSlot slot = TimeSlotGrid.Children[i] as TimeSlot;
if (slot is null) continue;
slot.Visibility = i <= currentHour ? Visibility.Visible : Visibility.Hidden;
slot.Background = Brushes.Gray;
if (i == currentHour)
{
lgb.GradientStops[1].Offset = currentHourPart;
lgb.GradientStops[2].Offset = currentHourPart;
slot.Background = lgb;
}
}
}
Модель представления (минимальная)
public class MainViewModel
{
public UIElements UiStuff { get; set; }
private readonly TraceCommunication Trace;
public MainViewModel()
{
Trace = new TraceCommunication();
Trace.MessageReceived += Trace_MessageReceived;
Trace.StartConnection("10.128.251.136", "9334");
}
private void Trace_MessageReceived(DataRow message)
{
if (!DateTime.TryParse(values[Columns.TimeStamp].ToString(), out DateTime dt)) return;
UiStuff.CurrentTime = dt;
}
}
public class UIElements : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
private DateTime currentTime;
public DateTime CurrentTime
{
get => currentTime;
set
{
currentTime = value;
CurrentTimeString = currentTime.ToString("HH:mm");
double timescale = currentTime.AddHours(-6).TimeOfDay.TotalMinutes / 1440;
TimePos = (int)(GridWidth * timescale);
OnPropertyChanged();
}
}
private string currenTimeString;
public string CurrentTimeString
{
get => currenTimeString;
set { currenTimeString = value; OnPropertyChanged();}
}
}
Как видно на скриншоте, привязки обновились только один раз после запуска (9:51). TimeSlots отображаются правильно (сейчас 10:30).
Как уже упоминалось, все это работает при использовании симулятора. Единственная разница заключается в объекте TraceCommunication, который контролирует удаленную систему. ViewModel подписывается на событие MessageReceived и соответствующим образом устанавливает элементы пользовательского интерфейса.
public class TraceCommunication
{
internal delegate void TraceEventHandler(DataRow value);
internal event TraceEventHandler MessageReceived;
private TraceEngineIntf traceEngine;
private string _Ip;
private string _Port;
public TraceCommunication()
{
//...
}
internal bool StartConnection(string ip, string port)
{
_Ip = ip;
_Port = port;
bool success = true;
if (!ip.Contains("."))
{
TraceSimulator Sim = new TraceSimulator(ip);
Sim.MessageReceived += FireEventIfRelevant;
Sim.StartTracing();
}
else
{
traceEngine = new TraceEngineIntf();
traceEngine.ConnectedChanged += Trace_ConnectedChanged;
traceEngine.MessageReceived += FireEventIfRelevant;
success = traceEngine.SetHost(_Ip, _Port);
}
return success;
}
private void FireEventIfRelevant(DataRow row)
{
if (!TraceFilter.IsRelevant(row)) return;
MessageReceived?.Invoke(row);
}
private void Trace_ConnectedChanged(object sender, EventArgs e)
{
traceEngine = sender as TraceEngineIntf;
if (traceEngine.Connected)
{
traceEngine.StartRead();
}
else
{
Connect();
}
}
}
В обоих случаях данные принимаются и обрабатываются корректно. События запускаются, а затем правильно обрабатываются в ViewModel. Но пользовательский интерфейс реагирует только при использовании симулятора. Так было не всегда. Я начал использовать только настоящую вещь, которая работала нормально. Позже я реализовал симулятор, когда мне нужно было быстрее получить больше данных для разработки.
У меня нет идей, что может вызвать такое поведение. Кто-нибудь знает, что происходит, или может подсказать, что проверить дальше?
Спасибо!
@MaxNumov На самом деле я не знаю. У меня нет доступа к TraceEngineIntf, поскольку он принадлежит третьей стороне. Когда «traceEngine» подключен, он начинает читать. Я добавил это в вопрос. Мой Sim.StartTracing() — это асинхронная пустота, которая периодически запускает события.
Вы можете проверить это с помощью отладки в своем коде. Если это не поток пользовательского интерфейса, вам следует обрабатывать обновления пользовательского интерфейса в потоке пользовательского интерфейса. В этом вопросе вы можете узнать, как проверить, нужно ли вам использовать вызов stackoverflow.com/questions/5436349/…
Если модели представления правильно реализуют inotifypropertychanged, то уведомление об изменении свойства будет распределяться по потокам. Я не вижу модели представления в вопросе. Много кода, но нет модели представления. Вы должны показать нам минимальное воспроизведение вашей проблемы, а не кучу кода. Я также предлагаю вам использовать snoop, чтобы проверить, что экземпляр модели представления, которая меняет свойства, действительно является контекстом данных.
@Andy Энди, я добавил к вопросу ViewModel. Я также проверил контекст данных с помощью snoop, это фактический экземпляр ViewModel с измененными свойствами.





Элемент управления TimeStrip содержит 24 элемента управления TimeSlot, представляющие часы дня. Он сломался, когда я установил для BackGround текущего TimeSlot значение LinearGradientBrush вместо сплошного Brush в методе обработчика Grid_LayoutUpdatedTimeStrip.
Я исправил это, передав «currentHourPart» в качестве свойства зависимости самому TimeSlot и соответствующим образом установив там градиент.
Я до сих пор не знаю, почему это вызвало проблему и почему это сработало, когда я использовал симулятор.
В каком потоке приложение получает события?