I am new .. or more precisely.. never used RX so I was wondering whether I can use it for this situation: I want to add a sort of Resharper Live Templates functionality to my app that allows users to enter short sequences of characters followed by a [Tab] and my app would replace the previously typed characters with the elsewhere specified, full text.
Now I have a list of character arrays, each of them representing one possible sequence. I want some sort of stopwords/-keys that break the chain (e.g. space).
I have an event that is raised on each KeyPress in my application, now (how) can I use RX to observe this event and check against that aforementioned list whether one of the sequences has been fulfilled and finally [Tab] has been pressed?
I don't know if it's too late, but I have an answer for you.
The Rx extension method you need to use is BufferWithCount.
I'll assume you know how to turn key press events into an IObservable<char>.
So given you have a list of character sequences that you want to detect and then perform an action I suggest using a Dictionary<string, Action> to hold this data, like so:
var matches = new Dictionary<string, Action>()
{
{ "ba", () => Console.WriteLine("ba") },
{ "aba", () => Console.WriteLine("aba") },
{ "baa", () => Console.WriteLine("baa") },
{ "abc\t", () => Console.WriteLine("abc\\t") },
};
So here's the Rx (and IEnumerable) queries required:
int max =
matches
.Select(m => m.Key.Length)
.Max();
IObservable<string> chords =
Enumerable
.Range(2, max - 1)
.Select(n => keys
.BufferWithCount(n, 1)
.Select(cs => new string(cs.ToArray())))
.Merge();
IObservable<Action> actions =
chords
.Where(s => matches.ContainsKey(s))
.Select(s => matches[s]);
So finally you just have an IObservable<Action> that you can subscribe to and you just invoke the Action.
If you want to test out that this works use the following code:
IConnectableObservable<char> keys = "ababc\tdabaababc\tebad"
.ToObservable()
.Publish();
//`.Publish()` makes a cold observable become hot,
// but you must call `Connect()` to start producing values.
//insert above `matches` definition here.
//insert above queries here.
actions.Subscribe(a => a());
keys.Connect();
The result should be:
ba
aba
abc\t
ba
aba
baa
ba
aba
abc\t
ba
Enjoy!
Related
This question already has answers here:
How to Count Duplicates in List with LINQ
(7 answers)
Closed 2 years ago.
I currently have what I believe is a lambda function with C# (fairly new to coding & haven't used a lambda function before so go easy), which adds duplicate strings (From FilteredList) in a list and counts the number of occurrences and stores that value in count. I only want the most used word from the list which I've managed to do by the "groups.OrderBy()... etc) line, however I'm pretty sure that I've made this very complicated for myself and very inefficient. As well as by adding the dictionary and the key value pairs.
var groups =
from s in FilteredList
group s by s into g
// orderby g descending
select new
{
Stuff = g.Key,
Count = g.Count()
};
groups = groups.OrderBy(g => g.Count).Reverse().Take(1);
var dictionary = groups.ToDictionary(g => g.Stuff, g => g.Count);
foreach (KeyValuePair<string, int> kvp in dictionary)
{
Console.WriteLine("Key = {0}, Value = {1}", kvp.Key, kvp.Value);
}
Would someone please either help me through this and explain a little bit of this too me or at least point me in the direction of some learning materials which may help me better understand this.
For extra info: The FilteredList comes from a large piece of external text, read into a List of strings (split by delimiters), minus a list of string stop words.
Also, if this is not a lambda function or I've got any of the info in here incorrect, please kindly correct me so I can fix the question to be more relevant & help me find an answer.
Thanks in advance.
Yes, I think you have overcomplicated it somewhat.. Assuming your list of words is like:
var words = new[] { "what's", "the", "most", "most", "most", "mentioned", "word", "word" };
You can get the most mentioned word with:
words.GroupBy(w => w).OrderByDescending(g => g.Count()).First().Key;
Of course, you'd probably want to assign it to a variable, and presentationally you might want to break it into multiple lines:
var mostFrequentWord = words
.GroupBy(w => w) //make a list of sublists of words, like a dictionary of word:list<word>
.OrderByDescending(g => g.Count()) //order by sublist count descending
.First() //take the first list:sublist
.Key; //take the word
The GroupBy produces a collection of IGroupings, which is like a Dictionary<string, List<string>>. It maps each word (the key of the dictionary) to a list of all the occurrences of that word. In my example data, the IGrouping with the Key of "most" will be mapped to a List<string> of {"most","most","most"} which has the highest count of elements at 3. If we OrderByDescending the grouping based on the Count() of each of the lists then take the First, we'll get the IGrouping with a Key of "most", so all we need to do to retrieve the actual word is pull the Key out
If the word is just one of the properties of a larger object, then you can .GroupBy(o => o.Word). If you want some other property from the IGrouping such as its first or last then you can take that instead of the Key, but bear in mind that the property you end up taking might be different each time unless you enforce ordering of the list inside the grouping
If you want to make this more efficient than you can install MoreLinq and use MaxBy; getting the Max word By the count of the lists means you can avoid a sort operation. You could also avoid LINQ and use a dictionary:
string[] words = new[] { "what", "is", "the", "most", "most", "most", "mentioned", "word", "word" };
var maxK = "";
var maxV = -1;
var d = new Dictionary<string, int>();
foreach(var w in words){
if(!d.ContainsKey(w))
d[w] = 0;
d[w]++;
if(d[w] > maxV){
maxK = w;
maxV = d[w];
}
}
Console.WriteLine(maxK);
This keeps a dictionary that counts words as it goes, and will be more efficient than the LINQ route as it needs only a single pass of the word list, plus the associated dictionary lookups in contrast to "convert wordlist to list of sublists, sort list of sublists by sublist count, take first list item"
This should work:
var mostPopular = groups
.GroupBy(item => new {item.Stuff, item.Count})
.Select(g=> g.OrderByDescending(x=> x.Count).FirstOrDefault())
.ToList();
OrderByDescending along with .First() combines your usage of OrderBy, Reverse() and Take.
First part is a Linq operation to read the groups from the FilteredList.
var groups =
from s in FilteredList
group s by s into g
// orderby g descending
select new
{
Stuff = g.Key,
Count = g.Count()
};
The Lambda usage starts when the => signal is used. Basically means it's going to be computed at run time and an object of that type/format is to be created.
Example on your code:
groups = groups.OrderBy(g => g.Count).Reverse().Take(1);
Reading this, it is going to have an object 'g' that represents the elements on 'groups' with a property 'Count'. Being a list, it allows the 'Reverse' to be applied and the 'Take' to get the first element only.
As for documentation, best to search inside Stack Overflow, please check these links:
C# Lambda expressions: Why should I use them? - StackOverflow
Lambda Expressions in C# - external
Using a Lambda Expression Over a List in C# - external
Second step: if the data is coming from an external source and there are no performance issues, you can leave the code to refactor onwards. A more detail data analysis needs to be made to ensure another algorithm works.
I have a stream of events:
event.EventTime: 1s-----2s----3s----4s----5s----6s---
stream: A-B-C--D-----------------E-F---G-H--
An event looks like this:
public class Event
{
public DateTime EventTime { get; set; }
public int Value { get; set; }
}
EventTime should correspond to a time at which the event arrives, but there can be a small delay. The events are not supposed to arrive out-of-order, though.
Now, when I specify an grouping interval, say 1 second, I expect the stream to be grouped like this
1s-------2s----3s----4s----5s-----6s---
[A-B-C]--[D]---[ ]---[ ]---[E-F]--[G-H]
(notice the empty intervals)
I have tried using Buffer, but sadly I need to partition by EventTime, not System.DateTime.Now. Even with boundaries, I'd need some kind of look-ahead since when I use Buffer(2,1) as boundary and compare [0] and [1], even though [1] succesfully breaks the buffer, it still gets inserted into the old one instead of the new one. I also tried GroupBy, but that yielded groups only after the input stream finished. Which should never happen. Then I tried some this thing:
var intervalStart = GetIntervalStartLocal(DateTime.Now) + intervalLength;
var intervals = Observable.Timer(intervalStart, intervalLength);
var eventsAsObservables = intervals.GroupJoin<long, Event, long, Event, (DateTime, IObservable<Event>)>(
data,
_ => Observable.Never<long>(),
_ => Observable.Never<Event>(),
(intervalNumber, events) => {
var currentIntervalStart = intervalStart + intervalNumber*intervalLength;
var eventsInInterval = events
.SkipWhile(e => GetIntervalStartLocal(e.EventTime) < currentIntervalStart)
.TakeWhile(e => GetIntervalStartLocal(e.EventTime) == currentIntervalStart);
return (currentIntervalStart, eventsInInterval);
});
var eventsForIntervalsAsObservables = eventsAsObservables.SelectMany(g => {
var lists = g.Item2.Aggregate(new List<Event>(), (es, e) => { es.Add(e); return es; });
return lists.Select(l => (intervalStart: g.Item1, events: l));
});
var task = eventsForIntervalsAsObservables.ForEachAsync(es => System.Console.WriteLine(
$"=[{es.intervalStart.TimeOfDay}]= " + string.Join("; ", es.events.Select(e => e.EventTime.TimeOfDay))));
await task;
I was thinking that I'd use GroupJoin which joins based on values. So first, I'll emit interval timestamps. Then, inside GroupJoins resultSelector, I'll compute a matching interval from each Event, using GetIntervalStartLocal function (truncates the date to an interval length). After that, I'll skip all the potential leftovers from a previous interval (SkipWhile expected interval is higher then actually computed from Event). Finally, I'll TakeWhile event computed interval matches expected.
However, there must be a problem before I even get to SkipWhile and TakeWhile, because resultSelector actually does not operate on all data from data, but ignores some, e.g. like this:
event.EventTime: 1s-----2s----3s----4s----5s----6s---
stream: A---C--D-------------------F-----H--
and then constructs (from what it operates on, correctly):
1s-----2s----3s----4s----5s---6s---
[A-C]--[D]---[ ]---[ ]---[F]--[H]--
I think I must be doing something terribly wrong here, because it shouldn't be that hard to do partitioning on a stream based on a stream event value.
You need to clarify what you want. Given this:
time : 1s-------2s----3s----4s----5s-----6s---
stream: A-B-C----D-----------------E-F----G-H-- (actual)
group : [A-B-C]--[D]---[ ]---[ ]---[E-F]--[G-H] (desired result)
It's not clear whether 'time' here is your event time-stamp, or actual time. If it's actual time, then that is of course impossible: You can't pass a list of ABC before C has arrived. If you're referring to your event time-stamp, then Buffer or perhaps Window will have to know when to stop, which isn't that easy to do.
GroupBy does work for me as follows:
var sampleSource = Observable.Interval(TimeSpan.FromMilliseconds(400))
.Timestamp()
.Select(t => new Event { EventTime = t.Timestamp.DateTime, Value = (int)t.Value });
sampleSource
.GroupBy(e => e.EventTime.Ticks / 10000000) //10M ticks per second
.Dump(); //LinqPad
The only problem with this is that each group doesn't have a close criteria, so it's a giant memory leak. So you can add a timer to close the groups:
sampleSource
.GroupBy(e => e.EventTime.Ticks / 10000000) //10M ticks per second
.Select(g => g.TakeUntil(Observable.Timer(TimeSpan.FromSeconds(2)))) //group closes 2 seconds after opening
.Dump(); //LinqPad
This closing also allows us to return lists with .ToList(), rather than Observables:
sampleSource
.GroupBy(e => e.EventTime.Ticks / 10000000) //10M ticks per second
.SelectMany(g => g.TakeUntil(Observable.Timer(TimeSpan.FromSeconds(2))).ToList())
.Dump(); //LinqPad
Given a class:
class Foo { DateTime Timestamp {get; set;} }
...and an IObservable<Foo>, with guaranteed monotonically increasing Timestamps, how can I generate an IObservable<IList<Foo>> chunked into Lists based on those Timestamps?
I.e. each IList<Foo> should have five seconds of events, or whatever. I know I can use Buffer with a TimeSpan overload, but I need to take the time from the events themselves, not the wall clock. (Unless there a clever way of providing an IScheduler here which uses the IObservable itself as the source of .Now?)
If I try to use the Observable.Buffer(this IObservable<Foo> source, IObservable<Foo> bufferBoundaries) overload like so:
IObservable<Foo> foos = //...;
var pub = foos.Publish();
var windows = pub.Select(x => new DateTime(
x.Ticks - x.Ticks % TimeSpan.FromSeconds(5).Ticks)).DistinctUntilChanged();
pub.Buffer(windows).Subscribe(x => t.Dump())); // linqpad
pub.Connect();
...then the IList instances contain the item that causes the window to be closed, but I really want this item to go into the next window/buffer.
E.g. with timestamps [0, 1, 10, 11, 15] you will get blocks of [[0], [1, 10], [11, 15]] instead of [[0, 1], [10, 11], [15]]
Here's an idea. The group key condition is the "window number" and I use GroupByUntil. This gives you the desired output in your example (and I've used an int stream just like that example - but you can substitute whatever you need to number your windows).
public class Tests : ReactiveTest
{
public void Test()
{
var scheduler = new TestScheduler();
var xs = scheduler.CreateHotObservable<int>(
OnNext(0, 0),
OnNext(1, 1),
OnNext(10, 10),
OnNext(11, 11),
OnNext(15, 15),
OnCompleted(16, 0));
xs.Publish(ps => // (1)
ps.GroupByUntil(
p => p / 5, // (2)
grp => ps.Where(p => p / 5 != grp.Key)) // (3)
.SelectMany(x => x.ToList())) // (4)
.Subscribe(Console.WriteLine);
scheduler.Start();
}
}
Notes
We publish the source stream because we will subscribe more than once.
This is a function to create a group key - use this to generate a window number from your item type.
This is the group termination condition - use this to inspect the source stream for an item in another window. Note that means a window won't close until an element outside of it arrives, or the source stream terminates. This is obvious if you think about it - your desired output requires consideration of next element after a window ends. Note if your source bears any relation to real time, you could merge this with an Observable.Timer+Select that outputs a null/default instance of your term to terminate the stream earlier.
SelectMany puts the groups into lists and flattens the stream.
This example will run in LINQPad quite nicely if you include nuget package rx-testing. New up a Tests instance and just run the Test() method.
I think James World's answer is neater/more readable, but for posterity, I've found another way to do this using Buffer():
IObservable<Foo> foos = //...;
var pub = foos.Publish();
var windows = pub.Select(x => new DateTime(
x.Ticks - x.Ticks % TimeSpan.FromSeconds(5).Ticks))
.DistinctUntilChanged().Publish.RefCount();
pub.Buffer(windows, x => windows).Subscribe(x => t.Dump()));
pub.Connect();
With 10m events, James' approach is more than 2.5x as fast (20s vs. 56s on my machine).
Window is a generalization of Buffer, and GroupJoin is a generalization of Window (and Join). When you write a Window or Buffer query and you find that notifications are being incorrectly included or excluded from the edges of the windows/lists, then redefine your query in terms of GroupJoin to take control over where edge notifications arrive.
Note that in order to make the closing notifications available to newly opened windows you must define your boundaries as windows of those notifications (the windowed data, not the boundary data). In your case, you cannot use a sequence of DateTime values as your boundaries, you must use a sequence of Foo objects instead. To accomplish this, I've replaced your Select->DistinctUntilChanged query with a Scan->Where->Select query.
var batches = foos.Publish(publishedFoos => publishedFoos
.Scan(
new { foo = (Foo)null, last = DateTime.MinValue, take = true },
(acc, foo) =>
{
var boundary = foo.Timestamp - acc.last >= TimeSpan.FromSeconds(5);
return new
{
foo,
last = boundary ? foo.Timestamp : acc.last,
take = boundary
};
})
.Where(a => a.take)
.Select(a => a.foo)
.Publish(boundaries => boundaries
.Skip(1)
.StartWith((Foo)null)
.GroupJoin(
publishedFoos,
foo => foo == null ? boundaries.Skip(1) : boundaries,
_ => Observable.Empty<Unit>(),
(foo, window) => (foo == null ? window : window.StartWith(foo)).ToList())))
.Merge()
.Replay(lists => lists.SkipLast(1)
.Select(list => list.Take(list.Count - 1))
.Concat(lists),
bufferSize: 1);
The Replay query at the end is only required if you expect the sequence to eventually end and you care about not dropping the last notification; otherwise, you could simply modify window.StartWith(foo) to window.StartWith(foo).SkipLast(1) to achieve the same basic results, though the last notification of the last buffer will be lost.
I was trying to implement instant search on a database table with 10000+ records.
The search starts when the text inside the search text box changes, when the search box becomes empty I want to call a different method that loads all the data.
Also if the user changes the search string while results for another search are being loaded, then the loading of the those results should stop in favor of the new search.
I implemented it like the following code, but I was wondering if there is a better or cleaner way to do it using Rx (Reactive Extension) operators, I feel that creating a second observable inside the subscribe method of the first observable is more imperative than declarative, and the same for that if statement.
var searchStream = Observable.FromEventPattern(s => txtSearch.TextChanged += s, s => txtSearch.TextChanged -= s)
.Throttle(TimeSpan.FromMilliseconds(300))
.Select(evt =>
{
var txtbox = evt.Sender as TextBox;
return txtbox.Text;
}
);
searchStream
.DistinctUntilChanged()
.ObserveOn(SynchronizationContext.Current)
.Subscribe(searchTerm =>
{
this.parties.Clear();
this.partyBindingSource.ResetBindings(false);
long partyCount;
var foundParties = string.IsNullOrEmpty(searchTerm) ? partyRepository.GetAll(out partyCount) : partyRepository.SearchByNameAndNotes(searchTerm);
foundParties
.ToObservable(Scheduler.Default)
.TakeUntil(searchStream)
.Buffer(500)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(searchResults =>
{
this.parties.AddRange(searchResults);
this.partyBindingSource.ResetBindings(false);
}
, innerEx =>
{
}
, () => { }
);
}
, ex =>
{
}
, () =>
{
}
);
The SearchByNameAndNotes method just returns an IEnumerable<Party> using SQLite by reading data from a data reader.
I think you want something like this. EDIT: From your comments, I see you have a synchronous repository API - I'll leave the asynchronous version in, and add a synchronous version afterwards. Notes inline:
Asynchronous Repository Version
An asynchronous repository interface could be something like this:
public interface IPartyRepository
{
Task<IEnumerable<Party>> GetAllAsync(out long partyCount);
Task<IEnumerable<Party>> SearchByNameAndNotesAsync(string searchTerm);
}
Then I refactor the query as:
var searchStream = Observable.FromEventPattern(
s => txtSearch.TextChanged += s,
s => txtSearch.TextChanged -= s)
.Select(evt => txtSearch.Text) // better to select on the UI thread
.Throttle(TimeSpan.FromMilliseconds(300))
.DistinctUntilChanged()
// placement of this is important to avoid races updating the UI
.ObserveOn(SynchronizationContext.Current)
.Do(_ =>
{
// I like to use Do to make in-stream side-effects explicit
this.parties.Clear();
this.partyBindingSource.ResetBindings(false);
})
// This is "the money" part of the answer:
// Don't subscribe, just project the search term
// into the query...
.Select(searchTerm =>
{
long partyCount;
var foundParties = string.IsNullOrEmpty(searchTerm)
? partyRepository.GetAllAsync(out partyCount)
: partyRepository.SearchByNameAndNotesAsync(searchTerm);
// I assume the intention of the Buffer was to load
// the data into the UI in batches. If so, you can use Buffer from nuget
// package Ix-Main like this to get IEnumerable<T> batched up
// without splitting it up into unit sized pieces first
return foundParties
// this ToObs gets us into the monad
// and returns IObservable<IEnumerable<Party>>
.ToObservable()
// the ToObs here gets us into the monad from
// the IEnum<IList<Party>> returned by Buffer
// and the SelectMany flattens so the output
// is IObservable<IList<Party>>
.SelectMany(x => x.Buffer(500).ToObservable())
// placement of this is again important to avoid races updating the UI
// erroneously putting it after the Switch is a very common bug
.ObserveOn(SynchronizationContext.Current);
})
// At this point we have IObservable<IObservable<IList<Party>>
// Switch flattens and returns the most recent inner IObservable,
// cancelling any previous pending set of batched results
// superceded due to a textbox change
// i.e. the previous inner IObservable<...> if it was incomplete
// - it's the equivalent of your TakeUntil, but a bit neater
.Switch()
.Subscribe(searchResults =>
{
this.parties.AddRange(searchResults);
this.partyBindingSource.ResetBindings(false);
},
ex => { },
() => { });
Synchronous Repository Version
An synchronous repository interface could be something like this:
public interface IPartyRepository
{
IEnumerable<Party> GetAll(out long partyCount);
IEnumerable<Party> SearchByNameAndNotes(string searchTerm);
}
Personally, I don't recommend a repository interface be synchronous like this. Why? It is typically going to do IO, so you will wastefully block a thread.
You might say the client could call from a background thread, or you could wrap their call in a task - but this is not the right way to go I think.
The client doesn't "know" you are going to block; it's not expressed in the contract
It should be the repository that handles the asynchronous aspect of the implementation - after all, how this is best achieved will only be known best by the repository implementer.
Anyway, accepting the above, one way to implement is like this (of course it's mostly similar to the async version so I've only annotated the differences):
var searchStream = Observable.FromEventPattern(
s => txtSearch.TextChanged += s,
s => txtSearch.TextChanged -= s)
.Select(evt => txtSearch.Text)
.Throttle(TimeSpan.FromMilliseconds(300))
.DistinctUntilChanged()
.ObserveOn(SynchronizationContext.Current)
.Do(_ =>
{
this.parties.Clear();
this.partyBindingSource.ResetBindings(false);
})
.Select(searchTerm =>
// Here we wrap the synchronous repository into an
// async call. Note it's simply not enough to call
// ToObservable(Scheduler.Default) on the enumerable
// because this can actually still block up to the point that the
// first result is yielded. Doing as we have here,
// we guarantee the UI stays responsive
Observable.Start(() =>
{
long partyCount;
var foundParties = string.IsNullOrEmpty(searchTerm)
? partyRepository.GetAll(out partyCount)
: partyRepository.SearchByNameAndNotes(searchTerm);
return foundParties;
}) // Note you can supply a scheduler, default is Scheduler.Default
.SelectMany(x => x.Buffer(500).ToObservable())
.ObserveOn(SynchronizationContext.Current))
.Switch()
.Subscribe(searchResults =>
{
this.parties.AddRange(searchResults);
this.partyBindingSource.ResetBindings(false);
},
ex => { },
() => { });
What I'm trying to achieve is to handle some complex key press and release sequence with Rx. I have some little experience with Rx, but it's clearly not enough for my current undertaking, so I'm here for some help.
My WinForms app is running in the background, only visible in a system tray. By a given key sequence I want to activate one of it's forms. Btw, to hook up to the global key presses I'm using a nice library http://globalmousekeyhook.codeplex.com/ I'm able to receive every key down and key up events, and while key is down multiple KeyDown events are produced (with a standard keyboard repeat rate).
One of example key sequence I want to capture is a quick double Ctrl + Insert key presses (like holding Ctrl key and pressing Insert twice in a given period of time). Here is what I have currently in my code:
var keyDownSeq = Observable.FromEventPattern<KeyEventArgs>(m_KeyboardHookManager, "KeyDown");
var keyUpSeq = Observable.FromEventPattern<KeyEventArgs>(m_KeyboardHookManager, "KeyUp");
var ctrlDown = keyDownSeq.Where(ev => ev.EventArgs.KeyCode == Keys.LControlKey).Select(_ => true);
var ctrlUp = keyUpSeq.Where(ev => ev.EventArgs.KeyCode == Keys.LControlKey).Select(_ => false);
But then I'm stuck. My idea is that I need somehow to keep track of if the Ctrl key is down. One way is to create some global variable for that, and update it in some Merge listener
Observable.Merge(ctrlDown, ctrlUp)
.Do(b => globabl_bool = b)
.Subscribe();
But I think it ruins the whole Rx approach. Any ideas on how to achieve that while staying in Rx paradigm?
Then while the Ctrl is down I need to capture two Insert presses within a given time. I was thinking about using the Buffer:
var insertUp = keyUpSeq.Where(ev => ev.EventArgs.KeyCode == Keys.Insert);
insertUp.Buffer(TimeSpan.FromSeconds(1), 2)
.Do((buffer) => { if (buffer.Count == 2) Debug.WriteLine("happened"); })
.Subscribe();
However I'm not sure if it's most efficient way, because Buffer will produce events every one second, even if there was no any key pressed. Is there a better way? And I also need to combine that with Ctrl down somehow.
So once again, I need to keep track of double Insert press while Ctrl is down. Am I going in the right direction?
P.S. another possible approach is to subscribe to Insert observable only while Ctrl is down. Not sure how to achieve that though. Maybe some ideas on this as well?
EDIT: Another problem I've found is that Buffer doesn't suit my needs exactly. The problem comes from the fact that Buffer produces samples every two seconds, and if my first press belongs to the first buffer, and second to the next one, then nothing happens. How to overcome that?
Firstly, welcome to the brain-bending magic of the Reactive Framework! :)
Try this out, it should get you started on what you're after - comments in line to describe whats going on:
using(var hook = new KeyboardHookListener(new GlobalHooker()))
{
hook.Enabled = true;
var keyDownSeq = Observable.FromEventPattern<KeyEventArgs>(hook, "KeyDown");
var keyUpSeq = Observable.FromEventPattern<KeyEventArgs>(hook, "KeyUp");
var ctrlPlus =
// Start with a key press...
from keyDown in keyDownSeq
// and that key is the lctrl key...
where keyDown.EventArgs.KeyCode == Keys.LControlKey
from otherKeyDown in keyDownSeq
// sample until we get a keyup of lctrl...
.TakeUntil(keyUpSeq
.Where(e => e.EventArgs.KeyCode == Keys.LControlKey))
// but ignore the fact we're pressing lctrl down
.Where(e => e.EventArgs.KeyCode != Keys.LControlKey)
select otherKeyDown;
using(var sub = ctrlPlus
.Subscribe(e => Console.WriteLine("CTRL+" + e.EventArgs.KeyCode)))
{
Console.ReadLine();
}
}
Now this doesn't do exactly what you specified, but with a little tweaking, it could be easily adapted. The key bit is the implicit SelectMany calls in the sequential from clauses of the combined linq query - as a result, a query like:
var alphamabits =
from keyA in keyDown.Where(e => e.EventArgs.KeyCode == Keys.A)
from keyB in keyDown.Where(e => e.EventArgs.KeyCode == Keys.B)
from keyC in keyDown.Where(e => e.EventArgs.KeyCode == Keys.C)
from keyD in keyDown.Where(e => e.EventArgs.KeyCode == Keys.D)
from keyE in keyDown.Where(e => e.EventArgs.KeyCode == Keys.E)
from keyF in keyDown.Where(e => e.EventArgs.KeyCode == Keys.F)
select new {keyA,keyB,keyC,keyD,keyE,keyF};
translates (very) roughly into:
if A, then B, then C, then..., then F -> return one {a,b,c,d,e,f}
Make sense?
(ok, since you've read this far...)
var ctrlinsins =
from keyDown in keyDownSeq
where keyDown.EventArgs.KeyCode == Keys.LControlKey
from firstIns in keyDownSeq
// optional; abort sequence if you leggo of left ctrl
.TakeUntil(keyUpSeq.Where(e => e.EventArgs.KeyCode == Keys.LControlKey))
.Where(e => e.EventArgs.KeyCode == Keys.Insert)
from secondIns in keyDownSeq
// optional; abort sequence if you leggo of left ctrl
.TakeUntil(keyUpSeq.Where(e => e.EventArgs.KeyCode == Keys.LControlKey))
.Where(e => e.EventArgs.KeyCode == Keys.Insert)
select "Dude, it happened!";
All right, I've come up with some solution. It works, but has some limits which I'll explain further. I'll not accept the answer for some time, maybe somebody else will offer a better and more generic way to solve this problem. Anyway, here's the current solution:
private IDisposable SetupKeySequenceListener(Keys modifierKey, Keys doubleClickKey, TimeSpan doubleClickDelay, Action<Unit> actionHandler)
{
var keyDownSeq = Observable.FromEventPattern<KeyEventArgs>(m_KeyboardHookManager, "KeyDown");
var keyUpSeq = Observable.FromEventPattern<KeyEventArgs>(m_KeyboardHookManager, "KeyUp");
var modifierIsPressed = Observable
.Merge(keyDownSeq.Where(ev => (ev.EventArgs.KeyCode | modifierKey) == modifierKey).Select(_ => true),
keyUpSeq.Where(ev => (ev.EventArgs.KeyCode | modifierKey) == modifierKey).Select(_ => false))
.DistinctUntilChanged()
.Do(b => Debug.WriteLine("Ctrl is pressed: " + b.ToString()));
var mainKeyDoublePressed = Observable
.TimeInterval(keyDownSeq.Where(ev => (ev.EventArgs.KeyCode | doubleClickKey) == doubleClickKey))
.Select((val) => val.Interval)
.Scan((ti1, ti2) => ti2)
.Do(ti => Debug.WriteLine(ti.ToString()))
.Select(ti => ti < doubleClickDelay)
.Merge(keyUpSeq.Where(ev => (ev.EventArgs.KeyCode | doubleClickKey) == doubleClickKey).Select(_ => false))
.Do(b => Debug.WriteLine("Insert double pressed: " + b.ToString()));
return Observable.CombineLatest(modifierIsPressed, mainKeyDoublePressed)
.ObserveOn(WindowsFormsSynchronizationContext.Current)
.Where((list) => list.All(elem => elem))
.Select(_ => Unit.Default)
.Do(actionHandler)
.Subscribe();
}
Usage:
var subscriptionHandler = SetupKeySequenceListener(
Keys.LControlKey | Keys.RControlKey,
Keys.Insert | Keys.C,
TimeSpan.FromSeconds(0.5),
_ => { WindowState = FormWindowState.Normal; Show(); Debug.WriteLine("IT HAPPENED"); });
Let me explain what's going on here, maybe it will be useful for some. I'm essentially setting up 3 Observables, one is for modifier key (modifierIsPressed), another for key which needs to be double-clicked when modifier is pressed to activate the sequence (mainKeyDoublePressed), and the last that combines the two first.
First one is pretty straightforward: just convert key presses and releases to bool (using the Select). DistinctUntilChanged is needed because of if user press and holds some key, multiple events are generated. What I'm getting in this observable is a sequence of booleans, saying if modifier key is down.
Then the most tricky one, where the main key is handled. Let's go step by step:
I'm using TimeInterval to replace key down (it's important) events with the timespans
Then I'm getting the actual timespans out with Select function (to prepare for the next step)
Then comes the most tricky thing, the Scan. What it does is takes each two consecutive elements from previous sequence (timespans in our case) and passes them into a function as two parameters. Output of that function (which has to be of the same type as the parameters, a timespan) is passed further. The function in my case does very simple thing: just returns the second parameter.
Why? It's time to remember my actual task here: to catch double press of some button which are close enough to each other in time (like in half of a second in my example). My input is a sequence of timespans which are saying how much time passed since the previous event has happened. That's why I need to wait for two events: first one will be usually long enough, because it will tell since when user pressed the key last time, which could be minutes or more. But if the user presses the key two times quickly, then the second timespan will be small, since it will tell the difference between these two last quick presses.
Sounds complicated, right? Then think about it in a simple way: Scan always combines two latest events. That's why it fits my needs in this case: I need to listen to double-click. If I'd need to wait for 3 consecutive presses, I'd be at a loss here. That's why I call this approach limited, and still wait if somebody will offer a better and more generic solution, to handle potentially any key combination.
Anyway, let's continue the explanation:
4.Select(ti => ti < doubleClickDelay): here I just convert the sequence from timestamps to booleans, passing true for quick enough consecutive events, and false for not quick enough ones.
5.Here's another trick: I'm merging boolean sequence from step 4 to the new one, where I listen to the key up events. Remember that the original sequence one was built from key down events, right? So here I'm essentially taking the same approach as with observable number one: passing true for key down and false for key up.
Then it becomes super-easy to use CombineLatest function, which takes the last events from each sequence and pass them further, as a List, to the Where function, which checks if all of them are true. That's how I achieve my goal: now I know when main key was pressed twice while modifier key is held down. Merging in the main key up event ensures that I clear the state, so the next presses of modifier key will not trigger the sequence.
So here we go, that's pretty much it. I'll post this, but will not accept, as I said before. I hope somebody will chime in and enlighten me. :)
Thanks in advance!