Refactor MainWindow with MVVM

- Add MVVM structure(BaseViewModel)
- Create ViewModel for MainWindow
- Refactor MainWindow.xaml to support MVVM
- Move PublicAPI implementation from MainViewModel to PublicAPIInstance
This commit is contained in:
Colin Liu 2016-02-18 19:30:36 +08:00
parent a0f556b2a9
commit 8621fe2e3c
7 changed files with 1153 additions and 739 deletions

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace Wox.Converters
{
class VisibilityConverter : ConvertorBase<VisibilityConverter>
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null || value == DependencyProperty.UnsetValue)
{
return null;
}
return bool.Parse(value.ToString()) ? Visibility.Visible : Visibility.Collapsed;
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
}

View File

@ -1,6 +1,7 @@
<Window x:Class="Wox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wox="clr-namespace:Wox"
xmlns:vm="clr-namespace:Wox.ViewModel" xmlns:converters="clr-namespace:Wox.Converters"
Title="Wox"
Topmost="True"
Loaded="MainWindow_OnLoaded"
@ -15,14 +16,32 @@
Style="{DynamicResource WindowStyle}"
Icon="Images\app.png"
AllowsTransparency="True"
Visibility="{Binding IsVisible,Converter={converters:VisibilityConverter}}"
>
<Window.InputBindings>
<KeyBinding Key="Esc" Command="{Binding EscCommand}"></KeyBinding>
</Window.InputBindings>
<Window.Resources>
<DataTemplate DataType="{x:Type vm:ResultPanelViewModel}">
<wox:ResultPanel></wox:ResultPanel>
</DataTemplate>
<converters:VisibilityConverter x:Key="VisibilityConverter" />
</Window.Resources>
<Border Style="{DynamicResource WindowBorderStyle}" MouseDown="Border_OnMouseDown">
<StackPanel Orientation="Vertical">
<TextBox Style="{DynamicResource QueryBoxStyle}" PreviewDragOver="TbQuery_OnPreviewDragOver" AllowDrop="True"
x:Name="tbQuery" PreviewKeyDown="TbQuery_OnPreviewKeyDown" TextChanged="TbQuery_OnTextChanged" />
<Line Style="{DynamicResource PendingLineStyle}" x:Name="progressBar" Y1="0" Y2="0" X2="100" Height="2" StrokeThickness="1"></Line>
<wox:ResultPanel x:Name="pnlResult" />
<wox:ResultPanel x:Name="pnlContextMenu" Visibility="Collapsed" />
<TextBox Style="{DynamicResource QueryBoxStyle}" Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PreviewDragOver="TbQuery_OnPreviewDragOver" AllowDrop="True"
x:Name="tbQuery" />
<Line Style="{DynamicResource PendingLineStyle}" x:Name="progressBar" Y1="0" Y2="0" X2="100" Height="2" StrokeThickness="1"
Visibility="{Binding IsProgressBarVisible,Converter={StaticResource VisibilityConverter}}">
<Line.ToolTip>
<ToolTip IsOpen="{Binding IsProgressBarTooltipVisible}"></ToolTip>
</Line.ToolTip>
</Line>
<ContentControl Content="{Binding SearchResultPanel}" Visibility="{Binding IsSearchResultPanelVisible,Converter={StaticResource VisibilityConverter}}">
</ContentControl>
<ContentControl Content="{Binding ActionPanel}" Visibility="{Binding IsActionPanelVisible,Converter={StaticResource VisibilityConverter}}">
</ContentControl>
</StackPanel>
</Border>
</Window>

File diff suppressed because it is too large Load Diff

309
Wox/PublicAPIInstance.cs Normal file
View File

@ -0,0 +1,309 @@
using NHotkey;
using NHotkey.Wpf;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using Wox.Core.Plugin;
using Wox.Core.Resource;
using Wox.Core.UserSettings;
using Wox.Helper;
using Wox.Infrastructure.Hotkey;
using Wox.Plugin;
using Wox.ViewModel;
namespace Wox
{
public class PublicAPIInstance : IPublicAPI
{
#region Constructor
public PublicAPIInstance(MainViewModel mainVM)
{
this.MainVM = mainVM;
ThreadPool.SetMaxThreads(30, 10);
ThreadPool.SetMinThreads(10, 5);
GlobalHotkey.Instance.hookedKeyboardCallback += KListener_hookedKeyboardCallback;
WebRequest.RegisterPrefix("data", new DataWebRequestFactory());
SetHotkey(UserSettingStorage.Instance.Hotkey, OnHotkey);
SetCustomPluginHotkey();
}
#endregion
#region Properties
private MainViewModel MainVM
{
get;
set;
}
#endregion
#region Public API
public void ChangeQuery(string query, bool requery = false)
{
this.MainVM.QueryText = query;
//Application.Current.Dispatcher.Invoke(() =>
//{
// tbQuery.CaretIndex = this.MainVM.QueryText.Length;
// if (requery)
// {
// TbQuery_OnTextChanged(null, null);
// }
//});
}
public void ChangeQueryText(string query, bool selectAll = false)
{
this.MainVM.QueryText = query;
//Application.Current.Dispatcher.Invoke(() =>
//{
// tbQuery.CaretIndex = tbQuery.Text.Length;
// if (selectAll)
// {
// tbQuery.SelectAll();
// }
//});
}
public void CloseApp()
{
//notifyIcon.Visible = false;
Application.Current.Shutdown();
}
public void RestarApp()
{
ProcessStartInfo info = new ProcessStartInfo
{
FileName = Application.ResourceAssembly.Location,
Arguments = SingleInstance<App>.Restart
};
Process.Start(info);
}
public void HideApp()
{
HideWox();
}
public void ShowApp()
{
ShowWox();
}
public void ShowMsg(string title, string subTitle, string iconPath)
{
Application.Current.Dispatcher.Invoke(() =>
{
var m = new Msg { Owner = Application.Current.MainWindow };
m.Show(title, subTitle, iconPath);
});
}
public void OpenSettingDialog(string tabName = "general")
{
Application.Current.Dispatcher.Invoke(() =>
{
SettingWindow sw = SingletonWindowOpener.Open<SettingWindow>(this);
sw.SwitchTo(tabName);
});
}
public void StartLoadingBar()
{
this.MainVM.IsProgressBarVisible = true;
}
public void StopLoadingBar()
{
this.MainVM.IsProgressBarVisible = false;
}
public void InstallPlugin(string path)
{
Application.Current.Dispatcher.Invoke(() => PluginManager.InstallPlugin(path));
}
public void ReloadPlugins()
{
Application.Current.Dispatcher.Invoke(() => PluginManager.Init(this));
}
public string GetTranslation(string key)
{
return InternationalizationManager.Instance.GetTranslation(key);
}
public List<PluginPair> GetAllPlugins()
{
return PluginManager.AllPlugins.ToList();
}
public event WoxKeyDownEventHandler BackKeyDownEvent;
public event WoxGlobalKeyboardEventHandler GlobalKeyboardEvent;
public event ResultItemDropEventHandler ResultItemDropEvent;
public void PushResults(Query query, PluginMetadata plugin, List<Result> results)
{
results.ForEach(o =>
{
o.PluginDirectory = plugin.PluginDirectory;
o.PluginID = plugin.ID;
o.OriginQuery = query;
});
this.MainVM.UpdateResultView(results, plugin, query);
}
public void ShowContextMenu(PluginMetadata plugin, List<Result> results)
{
if (results != null && results.Count > 0)
{
results.ForEach(o =>
{
o.PluginDirectory = plugin.PluginDirectory;
o.PluginID = plugin.ID;
});
this.MainVM.ActionPanel.Clear();
//TODO:Show Action Panel accordingly
//pnlContextMenu.Clear();
//pnlContextMenu.AddResults(results, plugin.ID);
//pnlContextMenu.Visibility = Visibility.Visible;
//pnlResult.Visibility = Visibility.Collapsed;
}
}
#endregion
#region Private Methods
private bool KListener_hookedKeyboardCallback(KeyEvent keyevent, int vkcode, SpecialKeyState state)
{
if (GlobalKeyboardEvent != null)
{
return GlobalKeyboardEvent((int)keyevent, vkcode, state);
}
return true;
}
private void HideWox()
{
UserSettingStorage.Instance.WindowLeft = this.MainVM.Left;
UserSettingStorage.Instance.WindowTop = this.MainVM.Top;
this.MainVM.IsVisible = false;
}
private void ShowWox(bool selectAll = true)
{
UserSettingStorage.Instance.IncreaseActivateTimes();
this.MainVM.IsVisible = true;
//TODO:Adjust window properties
//Left = GetWindowsLeft();
//Top = GetWindowsTop();
//Show();
//Activate();
//Focus();
//tbQuery.Focus();
//ResetQueryHistoryIndex();
//if (selectAll) tbQuery.SelectAll();
}
public void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
{
var hotkey = new HotkeyModel(hotkeyStr);
SetHotkey(hotkey, action);
}
public void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
{
string hotkeyStr = hotkey.ToString();
try
{
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
}
catch (Exception)
{
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
MessageBox.Show(errorMsg);
}
}
public void RemoveHotkey(string hotkeyStr)
{
if (!string.IsNullOrEmpty(hotkeyStr))
{
HotkeyManager.Current.Remove(hotkeyStr);
}
}
/// <summary>
/// Checks if Wox should ignore any hotkeys
/// </summary>
/// <returns></returns>
private bool ShouldIgnoreHotkeys()
{
//double if to omit calling win32 function
if (UserSettingStorage.Instance.IgnoreHotkeysOnFullscreen)
if (WindowIntelopHelper.IsWindowFullscreen())
return true;
return false;
}
private void SetCustomPluginHotkey()
{
if (UserSettingStorage.Instance.CustomPluginHotkeys == null) return;
foreach (CustomPluginHotkey hotkey in UserSettingStorage.Instance.CustomPluginHotkeys)
{
CustomPluginHotkey hotkey1 = hotkey;
SetHotkey(hotkey.Hotkey, delegate
{
if (ShouldIgnoreHotkeys()) return;
ShowApp();
ChangeQuery(hotkey1.ActionKeyword, true);
});
}
}
private void OnHotkey(object sender, HotkeyEventArgs e)
{
if (ShouldIgnoreHotkeys()) return;
ToggleWox();
e.Handled = true;
}
private void ToggleWox()
{
if (!MainVM.IsVisible)
{
ShowWox();
}
else
{
HideWox();
}
}
#endregion
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Wox.ViewModel
{
public class BaseViewModel : INotifyPropertyChanged
{
protected virtual void OnPropertyChanged(string propertyName)
{
if (null != this.PropertyChanged)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class RelayCommand : ICommand
{
private Action _action;
public RelayCommand(Action action)
{
this._action = action;
}
public virtual bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public virtual void Execute(object parameter)
{
if (null != this._action)
{
this._action();
}
}
}
}

View File

@ -0,0 +1,449 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Wox.Core.Plugin;
using Wox.Core.Resource;
using Wox.Infrastructure;
using Wox.Infrastructure.Hotkey;
using Wox.Plugin;
using Wox.Storage;
namespace Wox.ViewModel
{
public class MainViewModel : BaseViewModel
{
#region Private Fields
private ResultPanelViewModel _searchResultPanel;
private ResultPanelViewModel _actionPanel;
private string _queryText;
private bool _isVisible;
private bool _isSearchResultPanelVisible;
private bool _isActionPanelVisible;
private bool _isProgressBarVisible;
private bool _isProgressBarTooltipVisible;
private double _left;
private double _top;
private bool _queryHasReturn;
private Query _lastQuery = new Query();
private bool _ignoreTextChange;
private List<Result> CurrentContextMenus = new List<Result>();
private string _textBeforeEnterContextMenuMode;
#endregion
#region Constructor
public MainViewModel()
{
this.InitializeResultPanel();
this.InitializeActionPanel();
this.InitializeKeyCommands();
this._queryHasReturn = false;
}
#endregion
#region ViewModel Properties
public ResultPanelViewModel SearchResultPanel
{
get
{
return this._searchResultPanel;
}
}
public ResultPanelViewModel ActionPanel
{
get
{
return this._actionPanel;
}
}
public string QueryText
{
get
{
return this._queryText;
}
set
{
this._queryText = value;
OnPropertyChanged("QueryText");
this.HandleQueryTextUpdated();
}
}
public bool IsVisible
{
get
{
return this._isVisible;
}
set
{
this._isVisible = value;
OnPropertyChanged("IsVisible");
if (!value && this.IsActionPanelVisible)
{
this.BackToSearchMode();
}
}
}
public bool IsSearchResultPanelVisible
{
get
{
return this._isSearchResultPanelVisible;
}
set
{
this._isSearchResultPanelVisible = value;
OnPropertyChanged("IsSearchResultPanelVisible");
}
}
public bool IsActionPanelVisible
{
get
{
return this._isActionPanelVisible;
}
set
{
this._isActionPanelVisible = value;
OnPropertyChanged("IsActionPanelVisible");
}
}
public bool IsProgressBarVisible
{
get
{
return this._isProgressBarVisible;
}
set
{
this._isProgressBarVisible = value;
OnPropertyChanged("IsProgressBarVisible");
}
}
public bool IsProgressBarTooltipVisible
{
get
{
return this._isProgressBarTooltipVisible;
}
set
{
this._isProgressBarTooltipVisible = value;
OnPropertyChanged("IsProgressBarTooltipVisible");
}
}
public double Left
{
get
{
return this._left;
}
set
{
this._left = value;
OnPropertyChanged("Left");
}
}
public double Top
{
get
{
return this._top;
}
set
{
this._top = value;
OnPropertyChanged("Top");
}
}
public ICommand EscCommand
{
get;
set;
}
#endregion
#region Private Methods
private void InitializeKeyCommands()
{
this.EscCommand = new RelayCommand(() => {
if (this.IsActionPanelVisible)
{
this.BackToSearchMode();
}
else
{
this.IsVisible = false;
}
});
}
private void InitializeResultPanel()
{
this._searchResultPanel = new ResultPanelViewModel();
this.IsSearchResultPanelVisible = false;
this._searchResultPanel.ResultOpenedInPanel += (o, e) =>
{
if (e.HideWindow)
{
this.IsVisible = false;
}
UserSelectedRecordStorage.Instance.Add(e.Result.RawResult);
QueryHistoryStorage.Instance.Add(this.QueryText);
};
this._searchResultPanel.ResultActionPanelOpenedInPanel += (o, e) =>
{
this.ShowActionPanel(e.Result.RawResult);
};
}
private void ShowActionPanel(Result result)
{
if (result == null) return;
List<Result> results = PluginManager.GetContextMenusForPlugin(result);
results.ForEach(o =>
{
o.PluginDirectory = PluginManager.GetPluginForId(result.PluginID).Metadata.PluginDirectory;
o.PluginID = result.PluginID;
o.OriginQuery = result.OriginQuery;
});
results.Add(GetTopMostContextMenu(result));
_textBeforeEnterContextMenuMode = this.QueryText;
this._actionPanel.Clear();
this._actionPanel.AddResults(results, result.PluginID);
CurrentContextMenus = results;
this.IsActionPanelVisible = true;
this.IsSearchResultPanelVisible = false;
this.QueryText = "";
}
private Result GetTopMostContextMenu(Result result)
{
if (TopMostRecordStorage.Instance.IsTopMost(result))
{
return new Result(InternationalizationManager.Instance.GetTranslation("cancelTopMostInThisQuery"), "Images\\down.png")
{
PluginDirectory = WoxDirectroy.Executable,
Action = _ =>
{
TopMostRecordStorage.Instance.Remove(result);
//TODO:Modify the way showing this message
//ShowMsg("Succeed", "", "");
return false;
}
};
}
else
{
return new Result(InternationalizationManager.Instance.GetTranslation("setAsTopMostInThisQuery"), "Images\\up.png")
{
PluginDirectory = WoxDirectroy.Executable,
Action = _ =>
{
TopMostRecordStorage.Instance.AddOrUpdate(result);
//TODO:Modify the way showing this message
//ShowMsg("Succeed", "", "");
return false;
}
};
}
}
private void InitializeActionPanel()
{
this._actionPanel = new ResultPanelViewModel();
this.IsActionPanelVisible = false;
this._actionPanel.ResultOpenedInPanel += (o, e) =>
{
if (e.HideWindow)
{
this.IsVisible = false;
}
};
}
private void HandleQueryTextUpdated()
{
if (_ignoreTextChange) { _ignoreTextChange = false; return; }
this.IsProgressBarTooltipVisible = false;
if (this.IsActionPanelVisible)
{
QueryActionPanel();
}
else
{
string query = this.QueryText.Trim();
if (!string.IsNullOrEmpty(query))
{
Query(query);
//reset query history index after user start new query
ResetQueryHistoryIndex();
}
else
{
this._searchResultPanel.Clear();
}
}
}
private void QueryActionPanel()
{
var contextMenuId = "Context Menu Id";
this._actionPanel.Clear();
var query = this.QueryText.ToLower();
if (string.IsNullOrEmpty(query))
{
this._actionPanel.AddResults(CurrentContextMenus, contextMenuId);
}
else
{
List<Result> filterResults = new List<Result>();
foreach (Result contextMenu in CurrentContextMenus)
{
if (StringMatcher.IsMatch(contextMenu.Title, query)
|| StringMatcher.IsMatch(contextMenu.SubTitle, query))
{
filterResults.Add(contextMenu);
}
}
this._actionPanel.AddResults(filterResults, contextMenuId);
}
}
private void Query(string text)
{
_queryHasReturn = false;
var query = PluginManager.QueryInit(text);
if (query != null)
{
// handle the exclusiveness of plugin using action keyword
string lastKeyword = _lastQuery.ActionKeyword;
string keyword = query.ActionKeyword;
if (string.IsNullOrEmpty(lastKeyword))
{
if (!string.IsNullOrEmpty(keyword))
{
this._searchResultPanel.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata);
}
}
else
{
if (string.IsNullOrEmpty(keyword))
{
this._searchResultPanel.RemoveResultsFor(PluginManager.NonGlobalPlugins[lastKeyword].Metadata);
}
else if (lastKeyword != keyword)
{
this._searchResultPanel.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata);
}
}
_lastQuery = query;
Action action = new Action(async () =>
{
await Task.Delay(150);
if (!string.IsNullOrEmpty(query.RawQuery) && query.RawQuery == _lastQuery.RawQuery && !_queryHasReturn)
{
this.IsProgressBarTooltipVisible = true;
}
});
action.Invoke();
//Application.Current.Dispatcher.InvokeAsync(async () =>
//{
// await Task.Delay(150);
// if (!string.IsNullOrEmpty(query.RawQuery) && query.RawQuery == _lastQuery.RawQuery && !_queryHasReturn)
// {
// StartProgress();
// }
//});
PluginManager.QueryForAllPlugins(query);
}
this.IsProgressBarTooltipVisible = false;
}
private void ResetQueryHistoryIndex()
{
this._searchResultPanel.RemoveResultsFor(QueryHistoryStorage.MetaData);
QueryHistoryStorage.Instance.Reset();
}
private void UpdateResultViewInternal(List<Result> list, PluginMetadata metadata)
{
Stopwatch.Normal($"UI update cost for {metadata.Name}",
() => { this._searchResultPanel.AddResults(list, metadata.ID); });
}
private void BackToSearchMode()
{
this.QueryText = _textBeforeEnterContextMenuMode;
this.IsActionPanelVisible = false;
this.IsSearchResultPanelVisible = true;
}
#endregion
#region Public Methods
public void UpdateResultView(List<Result> list, PluginMetadata metadata, Query originQuery)
{
_queryHasReturn = true;
this.IsProgressBarTooltipVisible = false;
list.ForEach(o =>
{
o.Score += UserSelectedRecordStorage.Instance.GetSelectedCount(o) * 5;
});
if (originQuery.RawQuery == _lastQuery.RawQuery)
{
Application.Current.Dispatcher.Invoke(() => {
UpdateResultViewInternal(list, metadata);
});
}
if(list.Count > 0)
{
this.IsSearchResultPanelVisible = true;
}
}
#endregion
}
}

15
less.exe.stackdump Normal file
View File

@ -0,0 +1,15 @@
Stack trace:
Frame Function Args
00000010000 0018007208E (0018024F7D0, 00180215E59, 00000010000, 0000022B940)
00000010000 00180046DF2 (0000022C9A8, 001803253F8, 00000000001, 001803253F8)
00000010000 00180046E32 (00000000001, 00180325608, 00000010000, 00000000002)
00000010000 0018006C65D (001800CDED2, 00000000000, 00000000000, 00000000000)
0000022CBC0 0018006C6EE (00000000020, 00000000023, 00180047805, 00000000000)
0000022CBC0 001800475B7 (000002B6150, 00002080014, 0000022CDB0, 00000000000)
00000000000 0018004602C (00000000000, 00000000000, 00000000000, 00000000000)
00000000000 001800460C4 (00000000000, 00000000000, 00000000000, 00000000000)
00000000000 00100414FC1 (00000000000, 00000000000, 00000000000, 00000000000)
00000000000 00100401010 (00000000000, 00000000000, 00000000000, 00000000000)
00000000000 000779A59CD (00000000000, 00000000000, 00000000000, 00000000000)
00000000000 00077ADB981 (00000000000, 00000000000, 00000000000, 00000000000)
End of stack trace